# django_session

session_key: az3xx3319qp38rlwyytqphx7w52qsy0c
expire_date: 2023-07-31 04:12:40.864281
session_data:
  .eJxNUktv00AQTtuEPEiLQHBDQkg5tCBZ9SOOc0AqSQ9IoRRRUlou1tq7a2_iR_DaLYmEBAdKkVZcMvxG_ga7TiplLWvnm5nd-ebb-V77W9mplOsb7ItdFxV56BacZC7DILZ0EI82fB7ypySRgQM8QUmQan6a5BnzNJWiraNcO0kxiQZ3uQ82LggRD0Ec9Xqmh6weceyuYer00NL1PrL7OtWdvqlbluM4tu1j0-jTrulhYnm2R2Q-7joWdWwDRCtKA5aUl4J4smaDPS1WpbmkwgmIvRK5RTJj_jQisARRVVzkNlYHb-AnfADV-D2X5ygnEDZEq2R_VsIlHPxYxRHGLAngt9jGHog6JhQVUQ6iTRmJMHd95IdEphae2JbKjbZEY4Y4v04zqcDFzJtiarg8REbX7tiHanXm1pvj3snH4SWZnp8mw3fH9vvhpd1B8eDzeTqmF2-vP3mn-eshm08HX-dx8iWcUWswDl5mIxovkgV2XikdEM_dUgwQDSxJ5ywmm-YShq36v_rDant_9-kfCJ-JhvIv0kTFJGwqKFvOkcSjivp-SVmUNDfyF23GXV7MSFaKfSsaak-QqlL3Q5ROmLRalGWSyMpdO5N6pyCaJbm1bzj31FvVSIxYBOLx-qhu9Y8C5ZKzFEveqliOKJWFmtJGfs6uiAT3VUPuJGUJwRA-L3va22uryYXwRUm0fjeeYVUO3Wom3CuScZZKcWqWZmimeiBVY6XYrdiJeQCiAoX2Hy3LH70:1qLFbE:LCQX4oH_ucbpqp7-VrSVfuEjW8g8sTv26f20cbaszTo


session_key: 0gm4cvq52f4mwaq3chncsxkg2ojpxwll
expire_date: 2023-07-31 04:41:35.084277
session_data:
  .eJxNUk1v00AQTdqEfBBagUDihhA5EJCs2nEc54BU4h6QQimipLRcrLV3N97EH8FrtyQSEhwoRdpbht_IT-DKrpNKWcvaeTOzO2_ezvfqn9JuqVjf4Lm456I8C9yck9RlGERZB_Fgy-chf0ZiGejgKYonieYncZYyT1Mp2ibKteMEk3B4m7u_dUGAeADisN_vesjsE9vqGV2dHpi6PkDWQKe6PejqpmnbtmX5uGsMaK_rYWJ6lkdkPu7ZJrUtA0QzTCYsLi4F8WjDBntapEpzSYUTEHsFcvN4zvxZSGAFoqK4yG2sDl7DT_gAqvE7Ls9QRiCoi2bB_rSAK-j8WMcRxiyewG-xgz0QNUwoysMMRIsyEmLu-sgPiEzNPbEjlRuVRX2OOL9KUqnA-dybYWq4PEBGz2pbB2q1F-abo_7xR-eCzM5OYufdkfXeubDaKBp-PkvG9Pzt1SfvJHvtsMVs-HURxV-COTWH48nLdESjZbzE9iulA-KZW4gBoo4l6YxFZNtcgdOs_a3dr3SelR__g-CJqCv_MolVTMKGgrLlDEk8Kqnvl5RFSXMtf9Fi3OX5nKSF2DeirvYYqSo1P0DJlEmrSVkqiazd1VOpdwKiUZDb-JyFp96qSiLEQhAPN0d1c3A4US45S5HkrYpliFJZqCFt5GfskkhwVzXkThMWEwzB06Knvb2WmlwIXhREa7fjGVTk0K1nwr0kKWeJFKdqaobWVQ-kaqwVuxG7EZ-AKEGu_Qeq_yAu:1qLG3D:tl-AAiRKgtuXzPl2-5pnDr2Jx3q5ZrBoF-OlfBebD44


session_key: mk414n8hq73n42s8edosz1r4gu26co20
expire_date: 2023-08-02 02:38:11.203764
session_data:
  .eJxNUktv00AQTtOGPFqCxOOMkCJEQbLqRxzngFSSHpBCKaKktFystXfX3sSP4LVbEgkJDpQi7S3Dj-EXceFHsOukUtaydr6Z2Z1vvp1vtd-V7Uq5vsIzcddFRR66BSeZyzCILR3E_Q2fh_wpSWRgH09QEqSanyZ5xjxNpWjrKNeOU0yiwW3uvY0LQsRDEIe9nukhq0ccu2uYOj2wdL2P7L5Odadv6pblOI5t-9g0-rRrephYnu0RmY-7jkUd2wDRitKAJeWlIB6t2WBPi1VpLqlwAqJdIrdIZsyfRgSWIHYUF7mN1cFr-AHvQTV-x-U5ygmEDdEq2Z-WcAn731dxhDFLAvglqtgDUceEoiLKQexRRiLMXR_5IZGphSeqUrnRlmjMEOdXaSYVOJ95U0wNl4fI6Nod-0Ctztx6fdQ7_jC8INOzk2T49sh-N7ywOygefDpLx_T8zdVH7yR_NWTz6eDLPE4-hzNqDcbBi2xE40WywM5LpQPiuVuKAaKBJemcxWTTXMKwVf9bf1B9ulv99wfCx6Kh_Is0UTEJmwrKlnMk8aiivp9SFiXNtfzFHuMuL2YkK8W-EQ21J0hVqfshSidMWi3KMklk5a6dSr1TEM2S3No3nHvqrWokRiwC8XB9VLf6h4FyyVmKJW9VLEeUykJNaSM_Z5dEgl3VkDtJWUIwhE_KntrtPTW5ED4vidZvxzPckUO3mgn3kmScpVKcmqUZmqkeSNVYKXYjtmMegKhAof0HNKYgqg:1qLx4t:uggX3a0YpkRcHTMCPgCp2glHHid7Fgg8xPk47ZgzUFE


# blog_article

title: 111Gantt/BTS 生产计划电子看板甘特图
intro: “甘特图是一种用于说明项目进度的条形图。该图表在纵轴上列出了要执行的任务，在横轴上列出了时间间隔。图中水平条的宽度显示了每项活动的持续时间。”
created_time: 2021-11-15 08:59:05.403556
html_text:
  <h2 id="h2-u524Du8A00"><a name="前言" class="reference-link"></a><span class="header-link octicon octicon-link"></span>前言</h2><p>参考：<a href="https://www.freecodecamp.org/news/create-gantt-chart-using-css-grid/">https://www.freecodecamp.org/news/create-gantt-chart-using-css-grid/</a></p>
  <blockquote>
  <p>“甘特图是一种用于说明项目进度的条形图。该图表在纵轴上列出了要执行的任务，在横轴上列出了时间间隔。图中水平条的宽度显示了每项活动的持续时间。” – 维基百科</p>
  </blockquote>
  <h2 id="h2-u6210u54C1"><a name="成品" class="reference-link"></a><span class="header-link octicon octicon-link"></span>成品</h2><p><img src="https://img2020.cnblogs.com/blog/1252682/202107/1252682-20210701163224876-306973203.png" alt="">
  <p>计算思路<br>首先生成 7:00 - 6:00 24小时时间表头
  <pre><code class="lang-javascript">generateTimeRange () {
        // let h1 = parseInt(start.substring(0, 2))
        let timeRange = []
        for (let i = 7; i &lt; 24; i++) {
          timeRange.push((i &lt; 10 ? '0' + i : i) + ':' + '00')
          this.timeRangeOrigin.push(i)
        }
        for (let i = 0; i &lt; 7; i++) {
          timeRange.push((i &lt; 10 ? '0' + i : i) + ':' + '00')
          this.timeRangeOrigin.push(i)
        }
        return timeRange
      },
  </code></pre>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202106/1252682-20210603174950361-1632420587.png" alt="">
  <p>如第一条Item，30和0这两个条状图像，时间范围分别是 7:40 - 8:12 和 7:30 - 8:30</p>
  <p>如何让条状图形正好偏移到时间点呢？</p>
  <p>首先需要一个存储所以时间的数组，为了方便查找，里面的数据大概为: [7,8,9,10,11 …. 6]</p>
  <p>以第二条为例：</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202106/1252682-20210603175006725-1613106171.png" alt="">
  <p>180 这一条的起始时间为 2021-06-03 08:22:46 - 2021-06-03 11:49:15</p>
  <p>我们先来计算宽度，这个180该多长：</p>
  <p>let width = ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth<br>endIdx 和 startIdx 为结束时间的分钟在数组中的下标，itemWidth为每个单元格的宽度
  <p>那么上面的时间区间就是 (4 - 1) * 150 大约三个单元格的宽度</p>
  <p>那么再进行时间偏移</p>
  <p>180 的起始时间是8点，也就是需要跳跃7这个单元格到8，分钟时22分，那么算出偏移量</p>
  <p>(单元格宽度 / 60) * 分钟数 = 偏移量</p>
  <pre><code class="lang-javascript"> getOffset (minute) {
        return (this.itemWidth / 60) * minute
      },
  </code></pre>
  <p>那么最后还要处理一下宽度，这个时候的结束时间位置应该时不正确的。</p>
  <pre><code class="lang-javascript">let style = 'width:' + ((width - this.getOffset(parseInt(m1)) + this.getOffset(parseInt(m2))) - this.itemWidth) + 'px;'
  </code></pre>
  <p>最终宽度为: ((宽度 - 左偏移量 ) + 右偏移量) - 一个单元格宽度</p>
  <p>完整计算如下：</p>
  <pre><code class="lang-javascript"> getBarStyleVersion2 (flag, startTime, endTime) {
        let s = this.getMinutes(this.getTimestamp(startTime))
        let h1 = parseInt(s.substring(0, 2))
        let m1 = s.substring(s.indexOf(':') + 1, s.indexOf(':') + 3)
        let m2
        let h2
        if (endTime !== null) {
          let e = this.getMinutes(this.getTimestamp(endTime))
          h2 = parseInt(e.substring(0, 2))
          m2 = e.substring(s.indexOf(':') + 1, e.indexOf(':') + 3)
        } else {
          h2 = h1
          m2 = m1
        }
        let temp
        if (h1 &gt; h2) {
          temp = h1
          h1 = h2
          h2 = temp
        }
        let startIdx = this.getTimeIdx(h1)

        let endIdx = this.getTimeIdx(h2) + 1
        // 计算长度
        // // console.log(startIdx)
        // // console.log(endIdx)
        // // console.log('w:' + ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth)

        let width = ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth
        // console.log('offset:' + this.getOffset(parseInt(m1)))
        let marginLeft = 0

        if (startIdx &gt; 0) {
          marginLeft = (this.itemWidth * startIdx) + this.getOffset(parseInt(m1))
        } else {
          marginLeft = this.getOffset(parseInt(m1))
        }

        let style = 'width:' + ((width - this.getOffset(parseInt(m1)) + this.getOffset(parseInt(m2))) - this.itemWidth) + 'px;'
        // 计算margin
        style += 'margin-left:' + marginLeft + 'px;'
        // 计算偏移

        // console.log('m1: -&gt; ' + m1)
        // console.log('w: -&gt; ' + this.itemWidth)
        // // console.log('m2: -&gt; ' + m2)
        // // console.log(s)
        if (flag === 0) {
          style += 'background-color:#4F97A3;'
        } else {
          style += 'background-color:#0E4D92;'
        }
        return style
      },
      getOffset (minute) {
        return (this.itemWidth / 60) * minute
      },
  </code></pre>
  <h2 id="h2-u65F6u95F4u8F74"><a name="时间轴" class="reference-link"></a><span class="header-link octicon octicon-link"></span>时间轴</h2><p>就是图中的绿色竖线，代表当前时间。</p>
  <p>首先一个DIV</p>
  <p>然后生成样式：</p>
  <pre><code class="lang-javascript"> getNowLine () {
        let styles = 'border:1px dashed green;height: 900px;width: 1px; position: fixed;z-index:999;'
        let now = this.getMinutes(Date.parse(new Date()))
        let m1 = parseInt(now.substring(0, 2))
        let s1 = parseInt(now.substring(now.indexOf(':') + 1, now.indexOf(':') + 3))
        let idx = this.getTimeIdx(m1)

        styles += 'margin-left:' + ((this.itemWidth * (idx + 1)) + (150 - (60 - this.getOffset(s1)))) + 'px'
        // console.log(idx)
        return styles
      }
  </code></pre>
  <p>偏移量计算为 (单元格长度 * (小时数下标 + 1)) + (左侧固定栏宽度 - (60分钟 - 分钟数下标偏移量))</p>

markdown_text:
  ## 前言
  参考：https://www.freecodecamp.org/news/create-gantt-chart-using-css-grid/

  > “甘特图是一种用于说明项目进度的条形图。该图表在纵轴上列出了要执行的任务，在横轴上列出了时间间隔。图中水平条的宽度显示了每项活动的持续时间。” – 维基百科

  ## 成品
  ![](https://img2020.cnblogs.com/blog/1252682/202107/1252682-20210701163224876-306973203.png)

  计算思路
  首先生成 7:00 - 6:00 24小时时间表头

  ```javascript
  generateTimeRange () {
        // let h1 = parseInt(start.substring(0, 2))
        let timeRange = []
        for (let i = 7; i < 24; i++) {
          timeRange.push((i < 10 ? '0' + i : i) + ':' + '00')
          this.timeRangeOrigin.push(i)
        }
        for (let i = 0; i < 7; i++) {
          timeRange.push((i < 10 ? '0' + i : i) + ':' + '00')
          this.timeRangeOrigin.push(i)
        }
        return timeRange
      },
  ```

  ![](https://img2020.cnblogs.com/blog/1252682/202106/1252682-20210603174950361-1632420587.png)

  如第一条Item，30和0这两个条状图像，时间范围分别是 7:40 - 8:12 和 7:30 - 8:30

  如何让条状图形正好偏移到时间点呢？

  首先需要一个存储所以时间的数组，为了方便查找，里面的数据大概为: [7,8,9,10,11 .... 6]

  以第二条为例：

  ![](https://img2020.cnblogs.com/blog/1252682/202106/1252682-20210603175006725-1613106171.png)

  180 这一条的起始时间为 2021-06-03 08:22:46 - 2021-06-03 11:49:15

  我们先来计算宽度，这个180该多长：

  let width = ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth
  endIdx 和 startIdx 为结束时间的分钟在数组中的下标，itemWidth为每个单元格的宽度

  那么上面的时间区间就是 (4 - 1) * 150 大约三个单元格的宽度

  那么再进行时间偏移

  180 的起始时间是8点，也就是需要跳跃7这个单元格到8，分钟时22分，那么算出偏移量

  (单元格宽度 / 60) * 分钟数 = 偏移量

  ```javascript
   getOffset (minute) {
        return (this.itemWidth / 60) * minute
      },
  ```
  那么最后还要处理一下宽度，这个时候的结束时间位置应该时不正确的。

  ```javascript
  let style = 'width:' + ((width - this.getOffset(parseInt(m1)) + this.getOffset(parseInt(m2))) - this.itemWidth) + 'px;'
  ```

  最终宽度为: ((宽度 - 左偏移量 ) + 右偏移量) - 一个单元格宽度

  完整计算如下：

  ```javascript
   getBarStyleVersion2 (flag, startTime, endTime) {
        let s = this.getMinutes(this.getTimestamp(startTime))
        let h1 = parseInt(s.substring(0, 2))
        let m1 = s.substring(s.indexOf(':') + 1, s.indexOf(':') + 3)
        let m2
        let h2
        if (endTime !== null) {
          let e = this.getMinutes(this.getTimestamp(endTime))
          h2 = parseInt(e.substring(0, 2))
          m2 = e.substring(s.indexOf(':') + 1, e.indexOf(':') + 3)
        } else {
          h2 = h1
          m2 = m1
        }
        let temp
        if (h1 > h2) {
          temp = h1
          h1 = h2
          h2 = temp
        }
        let startIdx = this.getTimeIdx(h1)

        let endIdx = this.getTimeIdx(h2) + 1
        // 计算长度
        // // console.log(startIdx)
        // // console.log(endIdx)
        // // console.log('w:' + ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth)

        let width = ((endIdx - startIdx) === 0 ? 1 : (endIdx - startIdx)) * this.itemWidth
        // console.log('offset:' + this.getOffset(parseInt(m1)))
        let marginLeft = 0

        if (startIdx > 0) {
          marginLeft = (this.itemWidth * startIdx) + this.getOffset(parseInt(m1))
        } else {
          marginLeft = this.getOffset(parseInt(m1))
        }

        let style = 'width:' + ((width - this.getOffset(parseInt(m1)) + this.getOffset(parseInt(m2))) - this.itemWidth) + 'px;'
        // 计算margin
        style += 'margin-left:' + marginLeft + 'px;'
        // 计算偏移

        // console.log('m1: -> ' + m1)
        // console.log('w: -> ' + this.itemWidth)
        // // console.log('m2: -> ' + m2)
        // // console.log(s)
        if (flag === 0) {
          style += 'background-color:#4F97A3;'
        } else {
          style += 'background-color:#0E4D92;'
        }
        return style
      },
      getOffset (minute) {
        return (this.itemWidth / 60) * minute
      },
  ```
  ## 时间轴
  就是图中的绿色竖线，代表当前时间。

  首先一个DIV

  然后生成样式：

  ```javascript
   getNowLine () {
        let styles = 'border:1px dashed green;height: 900px;width: 1px; position: fixed;z-index:999;'
        let now = this.getMinutes(Date.parse(new Date()))
        let m1 = parseInt(now.substring(0, 2))
        let s1 = parseInt(now.substring(now.indexOf(':') + 1, now.indexOf(':') + 3))
        let idx = this.getTimeIdx(m1)

        styles += 'margin-left:' + ((this.itemWidth * (idx + 1)) + (150 - (60 - this.getOffset(s1)))) + 'px'
        // console.log(idx)
        return styles
      }
  ```
  偏移量计算为 (单元格长度 * (小时数下标 + 1)) + (左侧固定栏宽度 - (60分钟 - 分钟数下标偏移量))


title: 菲律宾博彩内幕与见闻
intro: ​ 那时候我收到了一个用户的私信，说是招人，我看了下他们公司的简介，好家伙，东南亚双龙集团，上万人的大公司，我来了兴趣，就和他聊了起来。 ​ 和我聊的人是那边......
created_time: 2021-11-15 13:50:18.565036
html_text:
  <p>​  2019年，那时我在合肥刚和老板掀桌辞职不干，待业在家，同时也在找工作。</p>
  <p>​ 那时候我收到了一个用户的私信，说是招人，我看了下他们公司的简介，东南亚双龙集团，上万人的大公司，我来了兴趣，就和他聊了起来。</p>
  <p>​ 和我聊的人是那边公司的一个部门经理，说看到我的简历，问我有没有意向。我没有拒绝，和他了解了一下那边的情况，比如薪资，是否安全，上班地点之类的。我得到的回复是，薪资15K，上班地点在菲律宾马尼拉帕塞，至于是否安全这个问题，这个确实不太好回答，他给我发了两个视频，是他们公司吸烟区，很多中国人，也夹杂着一些听不懂的当地语言。</p>
  <p>​ 对于当时19岁的我来说，15K那绝对是高薪资，而且包吃住，经理允诺的会给我一个单间，而且包签证和机票。</p>
  <p>​ 我承认我心动了，和家里人说了这件事，当然，我家里人那是极力反对。</p>
  <p>​ 我家里人也在用百度帮我搜了，拿给我看，都是什么被骗去就撕掉护照回不来，被绑架之类的。我自己也搜到了一些不太好的新闻，比如东方监狱，双龙妓院之类的。说是东方集团监狱式管理，双龙集团睡女下属(后来才知道，双龙妓院名不虚传)。</p>
  <p>​ 接下来我去办了护照，将护照寄到厦门，然后过几天，我收到了我的护照和一张签证，该准备启程了。</p>
  <p>​ 我家里人见我都办好了手续，也就没再说什么了。</p>
  <p>​ 机票是那边的财务帮我订的，从合肥飞广州，再从广州到马尼拉。</p>
  <p>​ 拎着包出了机场，看到了经理那一行人，他们来接的不止我一个，还有几个人。</p>
  <p>​ 怀着忐忑的心上了车，被带到一个饭店，应该是接风洗尘。这时的我可不敢是在国内跟老板掀桌子大不了走人那种心态了，毕竟在这个地方，出了事可以说叫天天不应，叫地地不灵。所以我尽量装的人畜无害，酒场上见人就喊哥，他们倒也客气，就是能感觉出有种江湖气。</p>
  <p>​ 经理叫安财，不用说肯定是假名字，他给了我一个小米手机和一张手机卡，让我换上，可以在这边上网和打电话，还认识了一个主管，我加他天哥，感觉上比较仗义。</p>
  <p>​ 接下来我被带到了宿舍，确实给了我一个单间，当时已经凌晨3点左右，于是就赶紧睡觉了。</p>
  <p>​ 第二天起来，被司机带到公司，公司是一个挺高的大厦，看上去周围都还是挺不错的，我没有工牌进不去。过了一会，财务人带我上楼，他叫做’朝鲜’，假名字，大家都这么叫。我被领上楼，收了我的护照，给了我一个工牌。这里的人有种调侃方式，叫工牌为狗牌，而那些做推广的，叫狗推。</p>
  <p>​ 说下这边的待遇吧。</p>
  <p>​ 每天10点到10点，10点去的时候可以吃顿饭，中午一顿，下午一顿，晚上一顿，夜宵一顿，然后如果到了12点还在加班，还能有一顿饭。</p>
  <p>​ 而且伙食还不错，菜很多，荤素齐全，免费拿。</p>
  <p>​ 交通方面，就是做班车。这里确实很多中国人，福建居多，基本各地都有，还有台湾人，韩国人，有些国人像个混混，言语粗鄙下流，也包括有些女生，所以这里鱼蛇混杂，什么都有。</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202111/1252682-20211101133353386-783870851.png" alt="">
  <p>​ 正式开始工作了，我的工作是引流，就是通过写程序的方式来给国内发广告，比如我的第一个任务是做视频裂变，什么是视频裂变呢，就是前几年你可能会在朋友圈看到一些用低俗的标题和视频来吸引用户点击，比如：</p>
  <p>​ ‘上市公司CEO伪装成保安被扇耳光’，‘密！老公不在家,XXXXXX’</p>
  <p>​ 当你视频看到一半，会弹出对话框，让你分享N个好友才能够继续观看，可能你分享了N个好友，还会让你继续分享，这就是裂变，从而实现一种病毒式的传播。</p>
  <p>​ 我负责这个项目的开发，其实那时候我看到微信关于分享的接口，已经被整改了，但我不敢说，先做出点东西再说。</p>
  <p>​ 于是做了一个将视频分享到朋友圈的demo，跟经理提了微信封了分享接口的事，于是这个项目就流产了。</p>
  <p>​ 接下来经理给了我一个discuz论坛，让我给这个论坛发所有用户发通知。</p>
  <p>​ 开始写爬虫，discuz论坛简直太容易被搞了，cookie劫持一下，然后所有用户id都是自增的，只要一个循环去调用接口去发就行了。</p>
  <p>​ 在这个公司终于能干上事了，其实那时候的discuz论坛已经没什么人用了，2019年很少有人上这种古老的论坛了，但咱也不敢说，说什么照做就是了。</p>
  <p>​ 经理给了我很多这种论坛，基于这个论坛，我开发了专门针对乐视论坛的爬虫，和一个多线程爬虫系统，对功能进行了增强，比如某个用户刚注册论坛，就会收到一则通知:</p>
  <p>​ ‘XXK.COM 充值送彩金，足彩XXX’</p>
  <p>​ 其实没什么用，基本都是僵尸用户，发的论坛很多，用户很多，实际上效果不大。</p>
  <p>​ 做到这里的时候我基本对这边的生活开始熟悉了，我圈子很小，在这里，沉默低调是我的生存法则。在来到这里没几天的时候，我在微信公众号上看到了一个视频，标题是中国程序员去菲律宾遭暴打！我点开视频一看那确实，然后我把视频拿给经理看，经理说这里不会的，这里是正规公司。</p>
  <p>​ 那就好。</p>
  <p>​ 这里有点枯燥，因为你基本不敢出去玩，这里本地人很穷，当时公司的保安，司机，和我们宿舍的保姆都是本地人。而且这边还有些恐怖的传言，比如从天哥哪里听来的：</p>
  <p>​ ‘某女生晚上单独出门，第二天被脱光扔公司门口。’</p>
  <p>​ ‘千人斩女HR，以陪睡引诱国人来菲律宾打工，然后转手将人卖到黑盘口，听说后来回国得了艾滋和癌症去世了’</p>
  <p>​ ‘坐黑车被拉到荒郊野外’</p>
  <p>​ 还有我有次下班，一群小孩马上围上我，很多双手在我口袋里进进出出，我可不敢怎么样，只拿紧了手机，因为不远处有两个虎视眈眈的本地人，最后还是有经验的同事来帮忙解围。</p>
  <p>​ 菲律宾是个禁烟的国家，在这里吸烟被看到可能会面临罚款，我有次刚出公司门口点了根烟，马上就有保安来沉着脸拿走了我的工牌，这时候也幸好，我的经理安财用1000披索贿赂了一下，搞定了这件事。</p>
  <p>​ 我在网上认识了一个网友，湖南人，我给他发了个定位，没想到他也给我回了个定位，好家伙，他也在菲律宾！他说他也是十几天前来的，但他做的是。。。忘了叫什么了，后来我的经理还让我请他一起吃饭，当然不会无缘无故吃饭，我们经理让我那个网友去窃取东方集团的客户资料，当然也是被我那个网友拒绝了，这要是被发现，后果不堪设想的啊。后来我回国后得知他还在菲律宾，现在他的微信注销了，最后一次聊天他说打算去日本，不知道他现在如何，希望他安好。</p>
  <p>​ 我也经常被他们拉着参加饭局，或者是聚餐，当然在饭桌上，我只管吃饭，其他时候闭上嘴就可以了。这边的海鲜很棒，螃蟹很大，算是饱了口福。</p>
  <p>​ 至于网赌是否靠谱，我这么说吧。据我的了解，分两种，一种杀猪盘，一种综合盘，杀猪盘那我想都知道，一开始给你点甜头，然后让你血本无归，还有就是我们这个部门的综合盘，里面很多赌博游戏，但是，你所有的数据都是可以被操纵的，比如某人在我们盘赌博赢了1K，这时候天哥就会说：</p>
  <p>​ “有个用户打了1000块给不给出？”</p>
  <p>​ 然后安财说：“给出，放长线钓大鱼”</p>
  <p>​ 偶尔有国内冤大头被这边狗推忽悠输了几十万，然后整个部门就放下工作去KTV。</p>
  <p>​ 好了，再说下我接下来的工作了，开始沾黄了。</p>
  <p>​ 经理弄了一个色情论坛，然后给了我一些网址，让我去将其他网站的视频爬过来同步到他的论坛内，这个也好办，色情视频基本都是m3u8格式，链接赤裸裸的暴露，爬虫很好写，然后将爬到的数据生成sql同步到公司数据库就可以了。</p>
  <p>​ 这时候经理还在twitter上找福利鸡谈商务合作，笑死我了，而且他有次气急败坏的说：</p>
  <p>​ “他妈的，来我们网站的都是小学生，一点钱都没有。”</p>
  <p>​ 色情论坛确实有蛮多用户注册，但基本都是些中小学生，一点钱都没有，又怎么能去赌呢。</p>
  <p>​ 我被调到了群体宿舍，因为安财，也就是我的经理和公司新招的一个女员工搞起来了，霸占了我的单间。</p>
  <p>​ 双龙妓院名不虚传。</p>
  <p>​ 这时候公司还在招人，来了一个海哥，大概30多岁，人看上去挺淳朴，会把从家乡带来的特产分给我们，据他所说他网赌欠了2百万，来菲律宾躲一躲，而他看到这边要长时间坐着的时候，表示不能干，他有腰间盘突出，不久后他提了离职，给批了，他还给我们秀他的蚂蚁分，负的，笑死了。</p>
  <p>​ 我一看给批了！那我也赶紧提了离职，然后经理找我谈了一轮话，好吧我应了下来，然后过两天，我又提了。</p>
  <p>​ 这次谈了很久，他也是在留我，但我去意已决，谈话过程也是温和很多，如果在国内我完全可以说，你们公司太垃圾我不想干，但在这我觉得还是不要这样的好，我一直强调，我技术太垃圾了，干不了，你们可以招个技术牛逼的。</p>
  <p>​ 好的，于是我也给批了。</p>
  <p>​ 当晚我就用我所有的披索买了很多酒，到宿舍喝了一晚，然后几天后，我拿到了护照，公司安排司机送我去机场，成功回了国。正好我回国后不久疫情爆发了，真是巧合。</p>
  <p>​ 后续，这个盘口倒闭了。</p>

markdown_text:
  ​  2019年，那时我在合肥刚和老板掀桌辞职不干，待业在家，同时也在找工作。

  ​ 那时候我收到了一个用户的私信，说是招人，我看了下他们公司的简介，东南亚双龙集团，上万人的大公司，我来了兴趣，就和他聊了起来。

  ​ 和我聊的人是那边公司的一个部门经理，说看到我的简历，问我有没有意向。我没有拒绝，和他了解了一下那边的情况，比如薪资，是否安全，上班地点之类的。我得到的回复是，薪资15K，上班地点在菲律宾马尼拉帕塞，至于是否安全这个问题，这个确实不太好回答，他给我发了两个视频，是他们公司吸烟区，很多中国人，也夹杂着一些听不懂的当地语言。

  ​ 对于当时19岁的我来说，15K那绝对是高薪资，而且包吃住，经理允诺的会给我一个单间，而且包签证和机票。

  ​ 我承认我心动了，和家里人说了这件事，当然，我家里人那是极力反对。

  ​ 我家里人也在用百度帮我搜了，拿给我看，都是什么被骗去就撕掉护照回不来，被绑架之类的。我自己也搜到了一些不太好的新闻，比如东方监狱，双龙妓院之类的。说是东方集团监狱式管理，双龙集团睡女下属(后来才知道，双龙妓院名不虚传)。

  ​ 接下来我去办了护照，将护照寄到厦门，然后过几天，我收到了我的护照和一张签证，该准备启程了。

  ​ 我家里人见我都办好了手续，也就没再说什么了。

  ​ 机票是那边的财务帮我订的，从合肥飞广州，再从广州到马尼拉。

  ​ 拎着包出了机场，看到了经理那一行人，他们来接的不止我一个，还有几个人。

  ​ 怀着忐忑的心上了车，被带到一个饭店，应该是接风洗尘。这时的我可不敢是在国内跟老板掀桌子大不了走人那种心态了，毕竟在这个地方，出了事可以说叫天天不应，叫地地不灵。所以我尽量装的人畜无害，酒场上见人就喊哥，他们倒也客气，就是能感觉出有种江湖气。

  ​ 经理叫安财，不用说肯定是假名字，他给了我一个小米手机和一张手机卡，让我换上，可以在这边上网和打电话，还认识了一个主管，我加他天哥，感觉上比较仗义。

  ​ 接下来我被带到了宿舍，确实给了我一个单间，当时已经凌晨3点左右，于是就赶紧睡觉了。

  ​ 第二天起来，被司机带到公司，公司是一个挺高的大厦，看上去周围都还是挺不错的，我没有工牌进不去。过了一会，财务人带我上楼，他叫做'朝鲜'，假名字，大家都这么叫。我被领上楼，收了我的护照，给了我一个工牌。这里的人有种调侃方式，叫工牌为狗牌，而那些做推广的，叫狗推。

  ​ 说下这边的待遇吧。

  ​ 每天10点到10点，10点去的时候可以吃顿饭，中午一顿，下午一顿，晚上一顿，夜宵一顿，然后如果到了12点还在加班，还能有一顿饭。

  ​ 而且伙食还不错，菜很多，荤素齐全，免费拿。

  ​ 交通方面，就是做班车。这里确实很多中国人，福建居多，基本各地都有，还有台湾人，韩国人，有些国人像个混混，言语粗鄙下流，也包括有些女生，所以这里鱼蛇混杂，什么都有。

  ![](https://img2020.cnblogs.com/blog/1252682/202111/1252682-20211101133353386-783870851.png)

  ​ 正式开始工作了，我的工作是引流，就是通过写程序的方式来给国内发广告，比如我的第一个任务是做视频裂变，什么是视频裂变呢，就是前几年你可能会在朋友圈看到一些用低俗的标题和视频来吸引用户点击，比如：

  ​ ‘上市公司CEO伪装成保安被扇耳光’，‘密！老公不在家,XXXXXX’

  ​ 当你视频看到一半，会弹出对话框，让你分享N个好友才能够继续观看，可能你分享了N个好友，还会让你继续分享，这就是裂变，从而实现一种病毒式的传播。

  ​ 我负责这个项目的开发，其实那时候我看到微信关于分享的接口，已经被整改了，但我不敢说，先做出点东西再说。

  ​ 于是做了一个将视频分享到朋友圈的demo，跟经理提了微信封了分享接口的事，于是这个项目就流产了。

  ​ 接下来经理给了我一个discuz论坛，让我给这个论坛发所有用户发通知。

  ​ 开始写爬虫，discuz论坛简直太容易被搞了，cookie劫持一下，然后所有用户id都是自增的，只要一个循环去调用接口去发就行了。

  ​ 在这个公司终于能干上事了，其实那时候的discuz论坛已经没什么人用了，2019年很少有人上这种古老的论坛了，但咱也不敢说，说什么照做就是了。

  ​ 经理给了我很多这种论坛，基于这个论坛，我开发了专门针对乐视论坛的爬虫，和一个多线程爬虫系统，对功能进行了增强，比如某个用户刚注册论坛，就会收到一则通知:

  ​ 'XXK.COM 充值送彩金，足彩XXX'

  ​ 其实没什么用，基本都是僵尸用户，发的论坛很多，用户很多，实际上效果不大。

  ​ 做到这里的时候我基本对这边的生活开始熟悉了，我圈子很小，在这里，沉默低调是我的生存法则。在来到这里没几天的时候，我在微信公众号上看到了一个视频，标题是中国程序员去菲律宾遭暴打！我点开视频一看那确实，然后我把视频拿给经理看，经理说这里不会的，这里是正规公司。

  ​ 那就好。

  ​ 这里有点枯燥，因为你基本不敢出去玩，这里本地人很穷，当时公司的保安，司机，和我们宿舍的保姆都是本地人。而且这边还有些恐怖的传言，比如从天哥哪里听来的：

  ​ ‘某女生晚上单独出门，第二天被脱光扔公司门口。’

  ​ ‘千人斩女HR，以陪睡引诱国人来菲律宾打工，然后转手将人卖到黑盘口，听说后来回国得了艾滋和癌症去世了’

  ​ ‘坐黑车被拉到荒郊野外’

  ​ 还有我有次下班，一群小孩马上围上我，很多双手在我口袋里进进出出，我可不敢怎么样，只拿紧了手机，因为不远处有两个虎视眈眈的本地人，最后还是有经验的同事来帮忙解围。

  ​ 菲律宾是个禁烟的国家，在这里吸烟被看到可能会面临罚款，我有次刚出公司门口点了根烟，马上就有保安来沉着脸拿走了我的工牌，这时候也幸好，我的经理安财用1000披索贿赂了一下，搞定了这件事。

  ​ 我在网上认识了一个网友，湖南人，我给他发了个定位，没想到他也给我回了个定位，好家伙，他也在菲律宾！他说他也是十几天前来的，但他做的是。。。忘了叫什么了，后来我的经理还让我请他一起吃饭，当然不会无缘无故吃饭，我们经理让我那个网友去窃取东方集团的客户资料，当然也是被我那个网友拒绝了，这要是被发现，后果不堪设想的啊。后来我回国后得知他还在菲律宾，现在他的微信注销了，最后一次聊天他说打算去日本，不知道他现在如何，希望他安好。

  ​ 我也经常被他们拉着参加饭局，或者是聚餐，当然在饭桌上，我只管吃饭，其他时候闭上嘴就可以了。这边的海鲜很棒，螃蟹很大，算是饱了口福。

  ​ 至于网赌是否靠谱，我这么说吧。据我的了解，分两种，一种杀猪盘，一种综合盘，杀猪盘那我想都知道，一开始给你点甜头，然后让你血本无归，还有就是我们这个部门的综合盘，里面很多赌博游戏，但是，你所有的数据都是可以被操纵的，比如某人在我们盘赌博赢了1K，这时候天哥就会说：

  ​ “有个用户打了1000块给不给出？”

  ​ 然后安财说：“给出，放长线钓大鱼”

  ​ 偶尔有国内冤大头被这边狗推忽悠输了几十万，然后整个部门就放下工作去KTV。

  ​ 好了，再说下我接下来的工作了，开始沾黄了。

  ​ 经理弄了一个色情论坛，然后给了我一些网址，让我去将其他网站的视频爬过来同步到他的论坛内，这个也好办，色情视频基本都是m3u8格式，链接赤裸裸的暴露，爬虫很好写，然后将爬到的数据生成sql同步到公司数据库就可以了。

  ​ 这时候经理还在twitter上找福利鸡谈商务合作，笑死我了，而且他有次气急败坏的说：

  ​ “他妈的，来我们网站的都是小学生，一点钱都没有。”

  ​ 色情论坛确实有蛮多用户注册，但基本都是些中小学生，一点钱都没有，又怎么能去赌呢。

  ​ 我被调到了群体宿舍，因为安财，也就是我的经理和公司新招的一个女员工搞起来了，霸占了我的单间。

  ​ 双龙妓院名不虚传。

  ​ 这时候公司还在招人，来了一个海哥，大概30多岁，人看上去挺淳朴，会把从家乡带来的特产分给我们，据他所说他网赌欠了2百万，来菲律宾躲一躲，而他看到这边要长时间坐着的时候，表示不能干，他有腰间盘突出，不久后他提了离职，给批了，他还给我们秀他的蚂蚁分，负的，笑死了。

  ​ 我一看给批了！那我也赶紧提了离职，然后经理找我谈了一轮话，好吧我应了下来，然后过两天，我又提了。

  ​ 这次谈了很久，他也是在留我，但我去意已决，谈话过程也是温和很多，如果在国内我完全可以说，你们公司太垃圾我不想干，但在这我觉得还是不要这样的好，我一直强调，我技术太垃圾了，干不了，你们可以招个技术牛逼的。

  ​ 好的，于是我也给批了。

  ​ 当晚我就用我所有的披索买了很多酒，到宿舍喝了一晚，然后几天后，我拿到了护照，公司安排司机送我去机场，成功回了国。正好我回国后不久疫情爆发了，真是巧合。

  ​ 后续，这个盘口倒闭了。


title: Python Django 搭建博客[开源]
intro: 博客搭建简单说明
created_time: 2021-11-17 13:02:29.967363
html_text:
  <h5 id="h5-u5F00u6E90u5730u5740"><a name="开源地址" class="reference-link"></a><span class="header-link octicon octicon-link"></span>开源地址</h5><p><a href="https://gitee.com/yang_chaojie/django-blog">https://gitee.com/yang_chaojie/django-blog</a></p>
  <h5 id="h5-u529Fu80FDu4ECBu7ECD"><a name="功能介绍" class="reference-link"></a><span class="header-link octicon octicon-link"></span>功能介绍</h5><p><img src="/upload/files/png/c984eeeb25ef41361c35cc7e2c99993c.png" alt="">
  <p>在右上角登录，主要功能就是分类，标签，和文章上传和音乐上传，NAS云盘等。</p>
  <p>文章格式使用Markdown，看一下写文章界面。</p>
  <p><img src="/upload/files/png/6fa46daed8ca6ac8cc6c87a3d8237028.png" alt="">
  <p>写文章有两种类型可选，一种是文章，一种是随笔，随笔的意思就类似QQ的说说，不会有摘要，而是直接显示全部内容，而文章需要点进去查看详情。</p>
  <p><img src="/upload/files/png/13c3379a368b94008437edeee4f6b298.png" alt="">
  <p>登录账号后，会出现管理选项。</p>
  <p><img src="/upload/files/png/a32ed788db57bb4a8bdc235f7f707baa.png" alt="">
  <p><img src="/upload/files/png/58e87531ae5e9a9e109bf16191d5a2d0.png" alt="">
  <p>点击分类和标签和查询到当前分类或标签下的所有文章。</p>
  <p><img src="/upload/files/png/539bffce57407318d00fc5c81939991f.png" alt="">
  <p>在部署的机器上的Nginx配置路径映射，可以方便的上传下载文件，并提供了文件的管理和删除。</p>
  <p><img src="/upload/files/png/b8335619b64aa3089adc65675ee892f6.png" alt="">
  <h2 id="h2-u90E8u7F72u914Du7F6E"><a name="部署配置" class="reference-link"></a><span class="header-link octicon octicon-link"></span>部署配置</h2><h5 id="h5-u521Du59CBu5316u9879u76EE"><a name="初始化项目" class="reference-link"></a><span class="header-link octicon octicon-link"></span>初始化项目</h5><p>需求：python3 pip3</p>
  <p>git 克隆项目到本地</p>
  <pre><code class="lang-bash">git clone https://gitee.com/yang_chaojie/django-blog.git
  </code></pre>
  <p>安装依赖：</p>
  <pre><code class="lang-bash">pip install -r requirements.txt
  </code></pre>
  <p>初始化Sqlite数据库：</p>
  <pre><code class="lang-bash">python manage.py makemigrations
  </code></pre>
  <pre><code class="lang-bash">python manage.py migrate
  </code></pre>
  <p>创建管理员账号：</p>
  <pre><code class="lang-bash">python manage.py createsuperuser
  </code></pre>
  <p>按照提示输入相应的用户名，邮箱和密码即可</p>
  <h5 id="h5-u914Du7F6Eu9879u76EE"><a name="配置项目" class="reference-link"></a><span class="header-link octicon octicon-link"></span>配置项目</h5><p>打开 settings.py</p>
  <pre><code class="lang-python">UPLOAD_URL = '/upload/files' 

  UPLOAD_ROOT = '/opt/IBE/files' #这里是你的文件上传路径

  DOMAIN = ''

  X_FRAME_OPTIONS = 'SAMEORIGIN'
  SECURE_CONTENT_TYPE_NOSNIFF = False
  NAS_PATH = '/media/pi/disk/' #这里是你的本机NAS映射路径
  </code></pre>
  <p>就可以了。</p>
  <p>在 linux 上运行 start.sh</p>
  <pre><code class="lang-shell">#!/bin/bash
  nohup python3 manage.py runserver 0.0.0.0:8000 --insecure &amp;
  </code></pre>
  <p>端口号是8000，可任意填写端口。</p>
  <p>NAS 配置路径：</p>
  <p>打开 nas.html</p>
  <pre><code class="lang-html">&lt;iframe src="http://s3.z100.vip:28592" width="100%" height="500px"&gt;&lt;/iframe&gt;
  </code></pre>
  <p>这里的: <a href="http://s3.z100.vip:28592">http://s3.z100.vip:28592</a> 是我通过Nginx映射出去的路径，同时内网穿透绑定的地址，你根据自己的情况填写。</p>
  <h5 id="h5-u6CE8u610Fu4E8Bu9879"><a name="注意事项" class="reference-link"></a><span class="header-link octicon octicon-link"></span>注意事项</h5><p>启动项目可能会报错，一般就俩。</p>
  <ol>
  <li>如果错误报在 manage.py ，那你需要用 python3 来执行命令，而不是python。</li><li>如果报Sqlite3版本相关错误，那么降低django版本至 2.1.8</li></ol>

markdown_text:
  ##### 开源地址

  https://gitee.com/yang_chaojie/django-blog

  ##### 功能介绍
  ![](/upload/files/png/c984eeeb25ef41361c35cc7e2c99993c.png)

  在右上角登录，主要功能就是分类，标签，和文章上传和音乐上传，NAS云盘等。

  文章格式使用Markdown，看一下写文章界面。

  ![](/upload/files/png/6fa46daed8ca6ac8cc6c87a3d8237028.png)

  写文章有两种类型可选，一种是文章，一种是随笔，随笔的意思就类似QQ的说说，不会有摘要，而是直接显示全部内容，而文章需要点进去查看详情。

  ![](/upload/files/png/13c3379a368b94008437edeee4f6b298.png)

  登录账号后，会出现管理选项。

  ![](/upload/files/png/a32ed788db57bb4a8bdc235f7f707baa.png)

  ![](/upload/files/png/58e87531ae5e9a9e109bf16191d5a2d0.png)

  点击分类和标签和查询到当前分类或标签下的所有文章。

  ![](/upload/files/png/539bffce57407318d00fc5c81939991f.png)


  在部署的机器上的Nginx配置路径映射，可以方便的上传下载文件，并提供了文件的管理和删除。

  ![](/upload/files/png/b8335619b64aa3089adc65675ee892f6.png)

  ## 部署配置

  ##### 初始化项目

  需求：python3 pip3

  git 克隆项目到本地

  ```bash
  git clone https://gitee.com/yang_chaojie/django-blog.git
  ```

  安装依赖：

  ```bash
  pip install -r requirements.txt
  ```

  初始化Sqlite数据库：

  ```bash
  python manage.py makemigrations
  ```

  ```bash
  python manage.py migrate
  ```

  创建管理员账号：

  ```bash
  python manage.py createsuperuser
  ```

  按照提示输入相应的用户名，邮箱和密码即可

  ##### 配置项目

  打开 settings.py

  ```python
  UPLOAD_URL = '/upload/files' 

  UPLOAD_ROOT = '/opt/IBE/files' #这里是你的文件上传路径

  DOMAIN = ''

  X_FRAME_OPTIONS = 'SAMEORIGIN'
  SECURE_CONTENT_TYPE_NOSNIFF = False
  NAS_PATH = '/media/pi/disk/' #这里是你的本机NAS映射路径
  ```

  就可以了。

  在 linux 上运行 start.sh

  ```shell
  #!/bin/bash
  nohup python3 manage.py runserver 0.0.0.0:8000 --insecure &
  ```

  端口号是8000，可任意填写端口。

  NAS 配置路径：

  打开 nas.html

  ```html
  <iframe src="http://s3.z100.vip:28592" width="100%" height="500px"></iframe>
  ```

  这里的: http://s3.z100.vip:28592 是我通过Nginx映射出去的路径，同时内网穿透绑定的地址，你根据自己的情况填写。



  ##### 注意事项

  启动项目可能会报错，一般就俩。

  1. 如果错误报在 manage.py ，那你需要用 python3 来执行命令，而不是python。
  2. 如果报Sqlite3版本相关错误，那么降低django版本至 2.1.8


title: JavaFx 语言播报 MTTS 实现
intro: JavaFx 通信ITC数字广播 SAPI 使用NeoSpeech Liang包生成语音
created_time: 2021-11-18 14:54:11.435457
html_text:
  <h2 id="h2-java-windows-sapi-spvoice-"><a name="Java调用Windows SAPI.spVoice。" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Java调用Windows SAPI.spVoice。</h2><p>Java作为跨平台语音对调用Windows是没有原生支持的，所以需要使用第三方框架来做中转实现目的。</p>
  <p>Jacob</p>
  <blockquote>
  <p>Jacob是一个Java库，可让Java应用程序与Microsoft Windows DLL或COM库进行通信。它通过使用定制的DLL来实现此目的，Jacob Java类通过JNI与之通信。Java库和dll将Java开发人员与基础Windows库隔离开来，因此Java开发人员不必编写自定义JNI代码。Jacob不用于创建ActiveX插件或Microsoft Windows应用程序内部的其他模块。</p>
  </blockquote>
  <p>项目地址：<a href="https://github.com/freemansoft/jacob-project">https://github.com/freemansoft/jacob-project</a></p>
  <h5 id="h5-u4F7Fu7528"><a name="使用" class="reference-link"></a><span class="header-link octicon octicon-link"></span>使用</h5><p>Jacob 1.8下载后里面拥有以下文件</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210203115320467-332248459.png" alt="">
  <p>将对应当前Java版本的DLL文件放到Java/bin目录，将jacob.jar作为项目依赖引入。</p>
  <h5 id="h5-sapi-microsoft-speech-api-"><a name="SAPI (Microsoft Speech API )" class="reference-link"></a><span class="header-link octicon octicon-link"></span>SAPI (Microsoft Speech API )</h5><p>文档：<a href="https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee125663(v=vs.85)?redirectedfrom=msdn">https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee125663(v=vs.85)?redirectedfrom=msdn</a></p>
  <p>使用 jacob 来调用SAPI。</p>
  <p>关于如何使用这里的博客讲的很详细:<a href="https://blog.csdn.net/asuyunlong/article/details/50083421/">https://blog.csdn.net/asuyunlong/article/details/50083421/</a></p>
  <p>但是我是用的是第三方语音库，在切换语音包时会报错。</p>
  <pre><code class="lang-java"> @Test
      public void test2() {
          MSTTSSpeech speech = new MSTTSSpeech();
          String[] vs = speech.getVoices();
          System.out.println(Arrays.toString(vs));
          String text = "TTS TEST";

          speech.setFormatType(6);
          speech.changeVoice(1);
          // speech.setRate(-1);
          // speech.setRate(-1);
          System.out.println(getClass().getResource("mp3").getPath() + "\\" + UUID.randomUUID() + ".wav");
          String url = getClass().getResource("mp3").getPath();
          File wavFile = new File(url);
          if (!wavFile.exists()) {
              wavFile.getParentFile().mkdirs();
          }

          speech.saveToWav(text, url.substring(1) + "\\" + UUID.randomUUID() + ".wav");
      }
  </code></pre>
  <p>运行测试后出错:</p>
  <pre><code class="lang-bash">A COM exception has been encountered:
  At Invoke of: Voice
  Description: 80020003 / 找不到成员。

  com.jacob.com.ComFailException: A COM exception has been encountered:
  At Invoke of: Voice
  Description: 80020003 / 找不到成员。

      at com.jacob.com.Dispatch.invokev(Native Method)
      ...
  </code></pre>
  <p><code>Description: 80020003 / 找不到成员。</code> 这个问题我找了很久，在我的电脑无法使用，但是可以使用控制面板项修改系统默认语音达到效果：</p>
  <p>C:\Windows\SysWOW64\Speech\SpeechUX\sapi.cpl</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210203115342594-921702312.png" alt="">
  <h2 id="h2-wav-mp3-mp3-"><a name="Wav转Mp3重新编码并合并多个Mp3文件" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Wav转Mp3重新编码并合并多个Mp3文件</h2><p><a href="https://lame.sourceforge.io/">https://lame.sourceforge.io/</a></p>
  <p>现在的需求是，我有一段这样的文本: [P]Call.mp3;[T] 呼叫 XXX 前往 SMT 3号线</p>
  <p>其中 [P]Call.mp3;[T] 是现有mp3文件，而其他文本是要通过sapi生成的，而且文本中可能还会出现外部的mp3文件。</p>
  <p>所以思路是将mp3文件名称和需要转语音的文本按顺序抽取出来，将文字转wav，wav转mp3，最后再合并mp3文件。</p>
  <p>代码 : AudioUtil4.java</p>
  <pre><code class="lang-java">public class AudioUtil4 {
      public static String path = null;
      public static String lame = null;
      static MSTTSSpeech speech = new MSTTSSpeech();

      static Logger LOG = LoggerFactory.getLogger(AudioUtil4.class);
      //合并mp3
      public static File mergeMp3(List&lt;PlayModel&gt; playModels) throws IOException {
          Vector&lt;FileInputStream&gt; fileInputStreams = new Vector&lt;&gt;();
          for (PlayModel playModel : playModels) {
              if (playModel.isMp3()) {
                  playModel.setPath(path + "\\" + playModel.getContent());
              } else {
                  playModel.setPath(AudioUtil4.textToMp3(playModel.getContent()));
              }
              fileInputStreams.add(new FileInputStream(playModel.getPath()));
             LOG.info("合并文件:" + playModel.getPath());
          }
          SequenceInputStream sequenceInputStream = new SequenceInputStream(fileInputStreams.elements());
          String mergePath = path + "\\" + UUID.randomUUID() + ".mp3";
          File afterMergeMp3 = new File(mergePath);
          boolean isSuccess = afterMergeMp3.createNewFile();
          if (isSuccess) {
              FileOutputStream fileOutputStream = new FileOutputStream(afterMergeMp3);//destinationfile
              int temp;

              while ((temp = sequenceInputStream.read()) != -1) {
                  // System.out.print( (char) temp ); // to print at DOS prompt
                  fileOutputStream.write(temp);   // to write to file
              }
              for (FileInputStream f : fileInputStreams) {
                  f.close();
              }
              fileOutputStream.close();
              sequenceInputStream.close();
              playModels.forEach(playModel -&gt; {
                  if (!playModel.isMp3()) {
                      new File(playModel.getPath()).delete();
                  }
              });
          } else {
              throw new IOException("mp3创建失败! " + mergePath);
          }
          return new File(mergePath);
      }
      //获取播放列表 抽取
      public static List&lt;PlayModel&gt; getPlayList(String message) {
          int i = 0;
          char[] messageArr = message.toCharArray();
          List&lt;PlayModel&gt; playModels = new ArrayList&lt;&gt;();
          StringBuilder temp = new StringBuilder();
          while (i &lt; messageArr.length) {
              if (messageArr[i] == '[') {
                  if (temp.length() &gt; 0) {
                      playModels.add(new PlayModel(false, temp.toString(), null));
                      temp = new StringBuilder();
                  }
                  Map&lt;String, Object&gt; mp3 = getEndIndex(i, messageArr);
                  i = (int) mp3.get("endIndex");
                  playModels.add(new PlayModel(true, mp3.get("mp3").toString(), null));
              } else {
                  temp.append(messageArr[i]);
              }
              i += 1;
          }
          if (temp.length() &gt; 0) {
              playModels.add(new PlayModel(false, temp.toString(), null));
          }
          return playModels;
      }
      //抽取
      public static Map&lt;String, Object&gt; getEndIndex(int index, char[] chars) {
          StringBuilder mp3 = new StringBuilder();
          int mp3EndIndex = -1;
          int endIndex = -1;
          for (int i = index + 3; i &lt; chars.length; i++) {
              if (chars[i] == ']') {
                  endIndex = i;
                  break;
              } else if (chars[i] == ';') {
                  mp3EndIndex = i;
              } else {
                  if (mp3EndIndex == -1) {
                      mp3.append(chars[i]);
                  }
              }
          }
          Map&lt;String, Object&gt; result = new HashMap&lt;&gt;();
          result.put("endIndex", endIndex);
          result.put("mp3", mp3);
          return result;
      }
      //wav转mp3
      public static String textToMp3(String text) throws FileNotFoundException {
         LOG.info("转语音:" + text);
          File wavFile = new File(textToWav(text));
          if (!wavFile.exists()) {
              throw new FileNotFoundException("转语音失败!");
          } else {
              //to mp3 use lame
              String wavPathStr = wavFile.getPath();
              String mp3PathStr = wavFile.getPath().
                      replace(".wav", ".mp3");
              String cmds1 = lame +
                      String.format(" -b128 --resample 24 -m j  %s %s", wavPathStr, mp3PathStr);
  //                Arguments = string.Format("-b128 --resample 24 -m j \"{0}\" \"{1}\"", str3, str4)
              //                    String.format(" --silent -b 160 -m m -q 9-resample -tt  %s %s", wavPathStr, mp3PathStr);

              String cmds2 = lame +
                      String.format(" --scale 4 --mp3input %s %s", mp3PathStr, mp3PathStr.replace(".mp3", "L.mp3"));

             LOG.info("lame to mp3 : " + cmds1);
             LOG.info("large voice : " + cmds2);
              CommandUtil.exec(cmds1);
              CommandUtil.exec(cmds2);
              new File(wavPathStr).delete();
              new File(mp3PathStr).delete();
              return mp3PathStr.replace(".mp3", "L.mp3");
          }
      }

      //文本转wav
      private static String textToWav(String text) {
          speech.setFormatType(16);
          File wavFile = new File(path);
          if (!wavFile.exists()) {
              wavFile.getParentFile().mkdirs();
          }
         LOG.info(path);
          String filePath = path + "\\" + UUID.randomUUID() + ".wav";
          speech.saveToWav(text, filePath);
         LOG.info("wav文件保存到 : " + filePath);
          return filePath;
      }
  </code></pre>
  <p>测试：</p>
  <pre><code class="lang-java"> @Test
      public void getPlayList() throws IOException {
          File directory = new File("");//设定为当前文件夹
          String mp3Path = directory.getAbsolutePath() + "\\mp3";
          String lamePath = directory.getAbsolutePath() + "\\lame.exe";

          AudioUtil4.path = mp3Path;
          AudioUtil4.lame = lamePath;


          List&lt;PlayModel&gt; playModels = AudioUtil4.getPlayList("[P]PromptRing.MP3;[T]请 袁振 速到 SMD 8号线 ");

          System.out.println("合并mp3:" + AudioUtil4.mergeMp3(playModels));
      }
  </code></pre>
  <p>命令行代码：</p>
  <p>CommandUtil.java</p>
  <pre><code class="lang-java">public class CommandUtil {
      static Logger LOG = LoggerFactory.getLogger(CommandUtil.class);

      public static int exec(String comm) {
          int finished = 0;
          try {
              Runtime rt = Runtime.getRuntime();
              Process proc = rt.exec(comm);
              InputStream stderr = proc.getErrorStream();
              InputStreamReader isr = new InputStreamReader(stderr);
              BufferedReader br = new BufferedReader(isr);
              String line = null;
              LOG.info("&lt;error&gt;&lt;/error&gt;");
              while ((line = br.readLine()) != null) {
                  String encoded = new String(line.getBytes("gbk"), "GB18030");//GB2312/CP936/GB18030
                  LOG.info(encoded);
                  LOG.info(line);
              }
              int exitVal = proc.waitFor();
              LOG.info("Process exitValue:" + exitVal);
              finished = exitVal;
          } catch (Exception e) {
              e.printStackTrace();
          }
          return finished;
      }
  }
  </code></pre>
  <p>Call Service 核心：</p>
  <pre><code class="lang-java">package cn.ciemis.service;

  import cn.ciemis.controller.MainController;
  import cn.ciemis.db.VoiceJDBC;
  import cn.ciemis.entity.*;
  import cn.ciemis.log.Log;
  import cn.ciemis.log.Logger;
  import cn.ciemis.sdk.IPCastSDK;
  import cn.ciemis.util.AudioUtil4;
  import cn.ciemis.util.FxUtil;
  import javafx.application.Platform;
  import javafx.concurrent.Service;
  import javafx.concurrent.Task;
  import org.controlsfx.control.StatusBar;
  import org.slf4j.LoggerFactory;

  import java.io.File;
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.sql.SQLException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Objects;

  import static cn.ciemis.controller.MainController.log;

  public class CallService extends Service&lt;String&gt; {

      Logger logger = new Logger(log, CallService.class.getName());
      org.slf4j.Logger LOG = LoggerFactory.getLogger(CallService.class);

      private StatusBar statusBar;

      private SettingModel settingModel;

      static IPCastSDK IPCastSDKObj = IPCastSDK.INSTANCE;
      public static IPCastSDK.IPCastCallBack ipcb;//回调函数

      //设置回调函数
      public static IPCastSDK.IPCastCallBack getIpcb() {
          return ipcb;
      }

      public static void setIpcb(IPCastSDK.IPCastCallBack ipcb) {
          CallService.ipcb = ipcb;
      }

      public static void SDKInit() {
          IPCastSDKObj.IPCAST_SDKInit();
      }

      public CallService(StatusBar statusBar) {
          this.statusBar = statusBar;
          try {
              this.settingModel = FxUtil.readIniFile();
          } catch (IOException | NoSuchFieldException | IllegalAccessException e) {
              logger.error("加载配置文件错误！");
              this.settingModel = null;
              e.printStackTrace();
          }
      }


      public void stop() {
          Platform.runLater(() -&gt; {
              statusBar.textProperty().unbind();
              statusBar.progressProperty().unbind();
              statusBar.progressProperty().setValue(0);
              statusBar.setText("已停止...");
              logger.warn("呼叫线程已停止");
              this.cancel();
              this.reset();
              statusBar.setText("正在重启...");
              this.restart();
          });
          if (IPCastSDKObj != null) {
              IPCastSDKObj.IPCAST_DisConnect();
          }

      }

      public void startSdk(SettingModel settingModel) {
          //设置回调函数
          try {
              SDKInit();
              boolean isConn = IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
              if (!isConn) {
                  logger.error("ICT服务器连接失败，检查连接参数是否正确");
  //                IPCastSDKObj.IPCAST_ExitStillPlay(1);
                  LOG.error("服务器连接失败!");
                  stop();
              } else {
                  setIpcb((EventNo, ParamStr) -&gt; {
                      LOG.debug(EventNo + " " + ParamStr);
                      return 0;
                  });
                  IPCastSDKObj.IPCAST_SetCallBack(ipcb);
                  LOG.info("连接成功！");
              }
              LOG.info(settingModel.getParamIp());
              LOG.info(settingModel.getParamUsername());
              LOG.info(settingModel.getDbPassword());
          } catch (Exception e) {
              logger.error("初始化DLL错误，检查DLL路径是否正确");
              e.printStackTrace();
              stop();
          }
      }


      public void clearMp3File() {
          File[] playedMp3;
          File file = new File(AudioUtil4.path);
          playedMp3 = file.listFiles();
          if (!Objects.isNull(playedMp3)) {
              for (File f : playedMp3) {
                  if (f.getName().contains(AudioUtil4.tail)) {
                      if (f.delete()) {
                          LOG.info("删除成功 : " + f.getAbsolutePath());
                      } else {
                          LOG.warn("删除失败 : " + f.getAbsolutePath());
                      }
                  }
              }
          }
      }

      public void sendPlay(String path, int[] pTList) throws UnsupportedEncodingException, InterruptedException {
          IPCastSDK.PlayFile.ByReference[] files = new IPCastSDK.PlayFile.ByReference[1];
          IPCastSDK.PlayFile.ByReference byReference1 = new IPCastSDK.PlayFile.ByReference();
          byReference1.fid = 0;
          byReference1.fname = path.getBytes("gbk");
          byReference1.fvol = 10;
          byReference1.write();
          files[0] = byReference1;
          int sid = -1;
          LOG.info(path);
          LOG.info("播放终端列表: " + Arrays.toString(pTList));
          sid = IPCastSDKObj.IPCAST_FilePlayStart(files, files.length, pTList, pTList.length, 500, IPCastSDK.PLAY_CYC_DANORDE, 0, 0);
          LOG.debug("SID:" + String.valueOf(sid));
      }

      public boolean getTermStatusEx(int[] tids) {
          boolean isIdle = false;
          IPCastSDK.TermAttr.ByReference term = new IPCastSDK.TermAttr.ByReference();
          for (int tid : tids) {
              if (IPCastSDKObj.IPCAST_GetTermStatusEx(tid, term)) {
                  if (term.status == 0) {
                      isIdle = true;
                  } else {
                      isIdle = false;
                      break;
                  }
              } else {
                  isIdle = false;
                  break;
              }
          }
          return isIdle;
      }

      @Override
      protected void executeTask(Task&lt;String&gt; task) {
          super.executeTask(task);
      }


      @Override
      protected void ready() {
          super.ready();
          LOG.info("ready" + Platform.isFxApplicationThread());
      }

      @Override
      protected void scheduled() {
          super.scheduled();
          LOG.info("scheduled " + Platform.isFxApplicationThread());
      }

      @Override
      protected void running() {
          super.running();
          logger.info("启动呼叫线程");
          LOG.info("running " + Platform.isFxApplicationThread());
      }

      @Override
      protected void succeeded() {
          super.succeeded();
          LOG.info("succeeded " + Platform.isFxApplicationThread());
      }

      @Override
      protected void cancelled() {
          super.cancelled();
          LOG.info("cancelled " + Platform.isFxApplicationThread());
      }

      @Override
      protected void failed() {
          super.failed();
          logger.info("Call Thread Start Failed ! Please Check You Config!");
          stop();
          LOG.info("failed " + Platform.isFxApplicationThread());
      }

      @Override
      protected Task&lt;String&gt; createTask() {
          return new Task&lt;String&gt;() {
              @Override
              protected String call() {
                  try {
                      updateMessage("初始化...");
                      startSdk(settingModel);
                      updateProgress(-1, 100);
                      VoiceJDBC voiceJDBC = new VoiceJDBC(settingModel);
                      updateMessage("Run...");
                      while (!isCancelled()) {
                          List&lt;MesAndonPlay&gt; mesAndonIssueDets = voiceJDBC.getTop5Voice();
                          for (MesAndonPlay u : mesAndonIssueDets) {
                              String[] terminalIds = u.getTerminalId().split(",");
                              int[] terminalIdsInt = new int[terminalIds.length];
                              for (int i = 0; i &lt; terminalIds.length; i++) {
                                  terminalIdsInt[i] = Integer.parseInt(terminalIds[i]);
                              }
                              boolean idle = getTermStatusEx(terminalIdsInt);
                              LOG.info("idle : " + idle);
                              if (idle) {
                                  if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                      IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
                                  }
                                  if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                      logger.info("无法连接广播 ");
                                      stop();
                                      break;
                                  }
                                  LOG.info(u.getPlayCommand());
                                  List&lt;PlayModel&gt; playModels = AudioUtil4.getPlayList(u.getPlayCommand());
                                  File path = AudioUtil4.mergeMp3(playModels);

                                  //播放
                                  sendPlay(path.getAbsolutePath(), terminalIdsInt);
                                  logger.warn(u.getPlayCommand());
                                  Thread.sleep(1000);
                                  voiceJDBC.finishDet(u.getId());
                              } else {
                                  updateProgress(-1, 100);
                                  updateMessage("正在呼叫...");
                              }
                          }
                          Thread.sleep(Integer.parseInt(settingModel.refreshTime.getValue()));
                          clearMp3File();
                          updateMessage("等待...");
                      }
                  } catch (SQLException | ClassNotFoundException throwables) {
                      logger.error(throwables.getMessage());
                      LOG.error(throwables.getMessage());
                      stop();
                      throwables.printStackTrace();
                  } catch (IOException ioException) {
                      logger.error("MP3生成失败!");
                      logger.error(ioException.getMessage());
                      LOG.error(ioException.getMessage());
                      stop();
                      ioException.printStackTrace();
                  } catch (Exception e) {
                      logger.error(e.getMessage());
                      LOG.error(e.getMessage());
                      stop();
                      e.printStackTrace();
                  }
                  //                catch (InterruptedException e) {
                  //                    logger.error("播放失败!");
                  //                    logger.error(e.getMessage());
                  //                    stop();
                  //                    e.printStackTrace();
                  //                }
                  return null;
              }
          };
      }
  }
  </code></pre>
  <h5 id="h5-javafx-log-"><a name="JavaFx Log日志输出" class="reference-link"></a><span class="header-link octicon octicon-link"></span>JavaFx Log日志输出</h5><p>界面 :</p>
  <p><img src="/upload/files/png/16934fdbc283e2f23b5daa126ae7d12f.png" alt="">
  <p>这个log日志的实现参考：<a href="https://stackoverflow.com/questions/24116858/most-efficient-way-to-log-messages-to-javafx-textarea-via-threads-with-simple-cu">https://stackoverflow.com/questions/24116858/most-efficient-way-to-log-messages-to-javafx-textarea-via-threads-with-simple-cu</a></p>
  <p>项目中使用了两个LOG，一个是GUI的LOG界面LogViewer，还有Log4j用来生成log文件。</p>
  <p>完毕。</p>

markdown_text:
  ## Java调用Windows SAPI.spVoice。

  Java作为跨平台语音对调用Windows是没有原生支持的，所以需要使用第三方框架来做中转实现目的。

  Jacob

  > Jacob是一个Java库，可让Java应用程序与Microsoft Windows DLL或COM库进行通信。它通过使用定制的DLL来实现此目的，Jacob Java类通过JNI与之通信。Java库和dll将Java开发人员与基础Windows库隔离开来，因此Java开发人员不必编写自定义JNI代码。Jacob不用于创建ActiveX插件或Microsoft Windows应用程序内部的其他模块。

  项目地址：https://github.com/freemansoft/jacob-project

  ##### 使用

  Jacob 1.8下载后里面拥有以下文件

  ![](https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210203115320467-332248459.png)

  将对应当前Java版本的DLL文件放到Java/bin目录，将jacob.jar作为项目依赖引入。

  ##### SAPI (Microsoft Speech API )

  文档：https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee125663(v=vs.85)?redirectedfrom=msdn

  使用 jacob 来调用SAPI。

  关于如何使用这里的博客讲的很详细:https://blog.csdn.net/asuyunlong/article/details/50083421/

  但是我是用的是第三方语音库，在切换语音包时会报错。

  ```java
   @Test
      public void test2() {
          MSTTSSpeech speech = new MSTTSSpeech();
          String[] vs = speech.getVoices();
          System.out.println(Arrays.toString(vs));
          String text = "TTS TEST";

          speech.setFormatType(6);
          speech.changeVoice(1);
          // speech.setRate(-1);
          // speech.setRate(-1);
          System.out.println(getClass().getResource("mp3").getPath() + "\\" + UUID.randomUUID() + ".wav");
          String url = getClass().getResource("mp3").getPath();
          File wavFile = new File(url);
          if (!wavFile.exists()) {
              wavFile.getParentFile().mkdirs();
          }

          speech.saveToWav(text, url.substring(1) + "\\" + UUID.randomUUID() + ".wav");
      }
  ```
  运行测试后出错:

  ```bash
  A COM exception has been encountered:
  At Invoke of: Voice
  Description: 80020003 / 找不到成员。

  com.jacob.com.ComFailException: A COM exception has been encountered:
  At Invoke of: Voice
  Description: 80020003 / 找不到成员。

  	at com.jacob.com.Dispatch.invokev(Native Method)
  	...
  ```

  `Description: 80020003 / 找不到成员。` 这个问题我找了很久，在我的电脑无法使用，但是可以使用控制面板项修改系统默认语音达到效果：

  C:\Windows\SysWOW64\Speech\SpeechUX\sapi.cpl

  ![](https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210203115342594-921702312.png)

  ## Wav转Mp3重新编码并合并多个Mp3文件

  https://lame.sourceforge.io/

  现在的需求是，我有一段这样的文本: [P]Call.mp3;[T] 呼叫 XXX 前往 SMT 3号线

  其中 [P]Call.mp3;[T] 是现有mp3文件，而其他文本是要通过sapi生成的，而且文本中可能还会出现外部的mp3文件。

  所以思路是将mp3文件名称和需要转语音的文本按顺序抽取出来，将文字转wav，wav转mp3，最后再合并mp3文件。

  代码 : AudioUtil4.java

  ```java
  public class AudioUtil4 {
      public static String path = null;
      public static String lame = null;
      static MSTTSSpeech speech = new MSTTSSpeech();

      static Logger LOG = LoggerFactory.getLogger(AudioUtil4.class);
  	//合并mp3
      public static File mergeMp3(List<PlayModel> playModels) throws IOException {
          Vector<FileInputStream> fileInputStreams = new Vector<>();
          for (PlayModel playModel : playModels) {
              if (playModel.isMp3()) {
                  playModel.setPath(path + "\\" + playModel.getContent());
              } else {
                  playModel.setPath(AudioUtil4.textToMp3(playModel.getContent()));
              }
              fileInputStreams.add(new FileInputStream(playModel.getPath()));
             LOG.info("合并文件:" + playModel.getPath());
          }
          SequenceInputStream sequenceInputStream = new SequenceInputStream(fileInputStreams.elements());
          String mergePath = path + "\\" + UUID.randomUUID() + ".mp3";
          File afterMergeMp3 = new File(mergePath);
          boolean isSuccess = afterMergeMp3.createNewFile();
          if (isSuccess) {
              FileOutputStream fileOutputStream = new FileOutputStream(afterMergeMp3);//destinationfile
              int temp;

              while ((temp = sequenceInputStream.read()) != -1) {
                  // System.out.print( (char) temp ); // to print at DOS prompt
                  fileOutputStream.write(temp);   // to write to file
              }
              for (FileInputStream f : fileInputStreams) {
                  f.close();
              }
              fileOutputStream.close();
              sequenceInputStream.close();
              playModels.forEach(playModel -> {
                  if (!playModel.isMp3()) {
                      new File(playModel.getPath()).delete();
                  }
              });
          } else {
              throw new IOException("mp3创建失败! " + mergePath);
          }
          return new File(mergePath);
      }
  	//获取播放列表 抽取
      public static List<PlayModel> getPlayList(String message) {
          int i = 0;
          char[] messageArr = message.toCharArray();
          List<PlayModel> playModels = new ArrayList<>();
          StringBuilder temp = new StringBuilder();
          while (i < messageArr.length) {
              if (messageArr[i] == '[') {
                  if (temp.length() > 0) {
                      playModels.add(new PlayModel(false, temp.toString(), null));
                      temp = new StringBuilder();
                  }
                  Map<String, Object> mp3 = getEndIndex(i, messageArr);
                  i = (int) mp3.get("endIndex");
                  playModels.add(new PlayModel(true, mp3.get("mp3").toString(), null));
              } else {
                  temp.append(messageArr[i]);
              }
              i += 1;
          }
          if (temp.length() > 0) {
              playModels.add(new PlayModel(false, temp.toString(), null));
          }
          return playModels;
      }
  	//抽取
      public static Map<String, Object> getEndIndex(int index, char[] chars) {
          StringBuilder mp3 = new StringBuilder();
          int mp3EndIndex = -1;
          int endIndex = -1;
          for (int i = index + 3; i < chars.length; i++) {
              if (chars[i] == ']') {
                  endIndex = i;
                  break;
              } else if (chars[i] == ';') {
                  mp3EndIndex = i;
              } else {
                  if (mp3EndIndex == -1) {
                      mp3.append(chars[i]);
                  }
              }
          }
          Map<String, Object> result = new HashMap<>();
          result.put("endIndex", endIndex);
          result.put("mp3", mp3);
          return result;
      }
  	//wav转mp3
      public static String textToMp3(String text) throws FileNotFoundException {
         LOG.info("转语音:" + text);
          File wavFile = new File(textToWav(text));
          if (!wavFile.exists()) {
              throw new FileNotFoundException("转语音失败!");
          } else {
              //to mp3 use lame
              String wavPathStr = wavFile.getPath();
              String mp3PathStr = wavFile.getPath().
                      replace(".wav", ".mp3");
              String cmds1 = lame +
                      String.format(" -b128 --resample 24 -m j  %s %s", wavPathStr, mp3PathStr);
  //                Arguments = string.Format("-b128 --resample 24 -m j \"{0}\" \"{1}\"", str3, str4)
              //                    String.format(" --silent -b 160 -m m -q 9-resample -tt  %s %s", wavPathStr, mp3PathStr);

              String cmds2 = lame +
                      String.format(" --scale 4 --mp3input %s %s", mp3PathStr, mp3PathStr.replace(".mp3", "L.mp3"));

             LOG.info("lame to mp3 : " + cmds1);
             LOG.info("large voice : " + cmds2);
              CommandUtil.exec(cmds1);
              CommandUtil.exec(cmds2);
              new File(wavPathStr).delete();
              new File(mp3PathStr).delete();
              return mp3PathStr.replace(".mp3", "L.mp3");
          }
      }

  	//文本转wav
      private static String textToWav(String text) {
          speech.setFormatType(16);
          File wavFile = new File(path);
          if (!wavFile.exists()) {
              wavFile.getParentFile().mkdirs();
          }
         LOG.info(path);
          String filePath = path + "\\" + UUID.randomUUID() + ".wav";
          speech.saveToWav(text, filePath);
         LOG.info("wav文件保存到 : " + filePath);
          return filePath;
      }

  ```

  测试：

  ```java
   @Test
      public void getPlayList() throws IOException {
          File directory = new File("");//设定为当前文件夹
          String mp3Path = directory.getAbsolutePath() + "\\mp3";
          String lamePath = directory.getAbsolutePath() + "\\lame.exe";

          AudioUtil4.path = mp3Path;
          AudioUtil4.lame = lamePath;


          List<PlayModel> playModels = AudioUtil4.getPlayList("[P]PromptRing.MP3;[T]请 袁振 速到 SMD 8号线 ");

          System.out.println("合并mp3:" + AudioUtil4.mergeMp3(playModels));
      }
  ```

  命令行代码：

  CommandUtil.java

  ```java
  public class CommandUtil {
      static Logger LOG = LoggerFactory.getLogger(CommandUtil.class);

      public static int exec(String comm) {
          int finished = 0;
          try {
              Runtime rt = Runtime.getRuntime();
              Process proc = rt.exec(comm);
              InputStream stderr = proc.getErrorStream();
              InputStreamReader isr = new InputStreamReader(stderr);
              BufferedReader br = new BufferedReader(isr);
              String line = null;
              LOG.info("<error></error>");
              while ((line = br.readLine()) != null) {
                  String encoded = new String(line.getBytes("gbk"), "GB18030");//GB2312/CP936/GB18030
                  LOG.info(encoded);
                  LOG.info(line);
              }
              int exitVal = proc.waitFor();
              LOG.info("Process exitValue:" + exitVal);
              finished = exitVal;
          } catch (Exception e) {
              e.printStackTrace();
          }
          return finished;
      }
  }
  ```

  Call Service 核心：
  ```java
  package cn.ciemis.service;

  import cn.ciemis.controller.MainController;
  import cn.ciemis.db.VoiceJDBC;
  import cn.ciemis.entity.*;
  import cn.ciemis.log.Log;
  import cn.ciemis.log.Logger;
  import cn.ciemis.sdk.IPCastSDK;
  import cn.ciemis.util.AudioUtil4;
  import cn.ciemis.util.FxUtil;
  import javafx.application.Platform;
  import javafx.concurrent.Service;
  import javafx.concurrent.Task;
  import org.controlsfx.control.StatusBar;
  import org.slf4j.LoggerFactory;

  import java.io.File;
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.sql.SQLException;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Objects;

  import static cn.ciemis.controller.MainController.log;

  public class CallService extends Service<String> {

      Logger logger = new Logger(log, CallService.class.getName());
      org.slf4j.Logger LOG = LoggerFactory.getLogger(CallService.class);

      private StatusBar statusBar;

      private SettingModel settingModel;

      static IPCastSDK IPCastSDKObj = IPCastSDK.INSTANCE;
      public static IPCastSDK.IPCastCallBack ipcb;//回调函数

      //设置回调函数
      public static IPCastSDK.IPCastCallBack getIpcb() {
          return ipcb;
      }

      public static void setIpcb(IPCastSDK.IPCastCallBack ipcb) {
          CallService.ipcb = ipcb;
      }

      public static void SDKInit() {
          IPCastSDKObj.IPCAST_SDKInit();
      }

      public CallService(StatusBar statusBar) {
          this.statusBar = statusBar;
          try {
              this.settingModel = FxUtil.readIniFile();
          } catch (IOException | NoSuchFieldException | IllegalAccessException e) {
              logger.error("加载配置文件错误！");
              this.settingModel = null;
              e.printStackTrace();
          }
      }


      public void stop() {
          Platform.runLater(() -> {
              statusBar.textProperty().unbind();
              statusBar.progressProperty().unbind();
              statusBar.progressProperty().setValue(0);
              statusBar.setText("已停止...");
              logger.warn("呼叫线程已停止");
              this.cancel();
              this.reset();
              statusBar.setText("正在重启...");
              this.restart();
          });
          if (IPCastSDKObj != null) {
              IPCastSDKObj.IPCAST_DisConnect();
          }

      }

      public void startSdk(SettingModel settingModel) {
          //设置回调函数
          try {
              SDKInit();
              boolean isConn = IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
              if (!isConn) {
                  logger.error("ICT服务器连接失败，检查连接参数是否正确");
  //                IPCastSDKObj.IPCAST_ExitStillPlay(1);
                  LOG.error("服务器连接失败!");
                  stop();
              } else {
                  setIpcb((EventNo, ParamStr) -> {
                      LOG.debug(EventNo + " " + ParamStr);
                      return 0;
                  });
                  IPCastSDKObj.IPCAST_SetCallBack(ipcb);
                  LOG.info("连接成功！");
              }
              LOG.info(settingModel.getParamIp());
              LOG.info(settingModel.getParamUsername());
              LOG.info(settingModel.getDbPassword());
          } catch (Exception e) {
              logger.error("初始化DLL错误，检查DLL路径是否正确");
              e.printStackTrace();
              stop();
          }
      }


      public void clearMp3File() {
          File[] playedMp3;
          File file = new File(AudioUtil4.path);
          playedMp3 = file.listFiles();
          if (!Objects.isNull(playedMp3)) {
              for (File f : playedMp3) {
                  if (f.getName().contains(AudioUtil4.tail)) {
                      if (f.delete()) {
                          LOG.info("删除成功 : " + f.getAbsolutePath());
                      } else {
                          LOG.warn("删除失败 : " + f.getAbsolutePath());
                      }
                  }
              }
          }
      }

      public void sendPlay(String path, int[] pTList) throws UnsupportedEncodingException, InterruptedException {
          IPCastSDK.PlayFile.ByReference[] files = new IPCastSDK.PlayFile.ByReference[1];
          IPCastSDK.PlayFile.ByReference byReference1 = new IPCastSDK.PlayFile.ByReference();
          byReference1.fid = 0;
          byReference1.fname = path.getBytes("gbk");
          byReference1.fvol = 10;
          byReference1.write();
          files[0] = byReference1;
          int sid = -1;
          LOG.info(path);
          LOG.info("播放终端列表: " + Arrays.toString(pTList));
          sid = IPCastSDKObj.IPCAST_FilePlayStart(files, files.length, pTList, pTList.length, 500, IPCastSDK.PLAY_CYC_DANORDE, 0, 0);
          LOG.debug("SID:" + String.valueOf(sid));
      }

      public boolean getTermStatusEx(int[] tids) {
          boolean isIdle = false;
          IPCastSDK.TermAttr.ByReference term = new IPCastSDK.TermAttr.ByReference();
          for (int tid : tids) {
              if (IPCastSDKObj.IPCAST_GetTermStatusEx(tid, term)) {
                  if (term.status == 0) {
                      isIdle = true;
                  } else {
                      isIdle = false;
                      break;
                  }
              } else {
                  isIdle = false;
                  break;
              }
          }
          return isIdle;
      }

      @Override
      protected void executeTask(Task<String> task) {
          super.executeTask(task);
      }


      @Override
      protected void ready() {
          super.ready();
          LOG.info("ready" + Platform.isFxApplicationThread());
      }

      @Override
      protected void scheduled() {
          super.scheduled();
          LOG.info("scheduled " + Platform.isFxApplicationThread());
      }

      @Override
      protected void running() {
          super.running();
          logger.info("启动呼叫线程");
          LOG.info("running " + Platform.isFxApplicationThread());
      }

      @Override
      protected void succeeded() {
          super.succeeded();
          LOG.info("succeeded " + Platform.isFxApplicationThread());
      }

      @Override
      protected void cancelled() {
          super.cancelled();
          LOG.info("cancelled " + Platform.isFxApplicationThread());
      }

      @Override
      protected void failed() {
          super.failed();
          logger.info("Call Thread Start Failed ! Please Check You Config!");
          stop();
          LOG.info("failed " + Platform.isFxApplicationThread());
      }

      @Override
      protected Task<String> createTask() {
          return new Task<String>() {
              @Override
              protected String call() {
                  try {
                      updateMessage("初始化...");
                      startSdk(settingModel);
                      updateProgress(-1, 100);
                      VoiceJDBC voiceJDBC = new VoiceJDBC(settingModel);
                      updateMessage("Run...");
                      while (!isCancelled()) {
                          List<MesAndonPlay> mesAndonIssueDets = voiceJDBC.getTop5Voice();
                          for (MesAndonPlay u : mesAndonIssueDets) {
                              String[] terminalIds = u.getTerminalId().split(",");
                              int[] terminalIdsInt = new int[terminalIds.length];
                              for (int i = 0; i < terminalIds.length; i++) {
                                  terminalIdsInt[i] = Integer.parseInt(terminalIds[i]);
                              }
                              boolean idle = getTermStatusEx(terminalIdsInt);
                              LOG.info("idle : " + idle);
                              if (idle) {
                                  if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                      IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
                                  }
                                  if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                      logger.info("无法连接广播 ");
                                      stop();
                                      break;
                                  }
                                  LOG.info(u.getPlayCommand());
                                  List<PlayModel> playModels = AudioUtil4.getPlayList(u.getPlayCommand());
                                  File path = AudioUtil4.mergeMp3(playModels);

                                  //播放
                                  sendPlay(path.getAbsolutePath(), terminalIdsInt);
                                  logger.warn(u.getPlayCommand());
                                  Thread.sleep(1000);
                                  voiceJDBC.finishDet(u.getId());
                              } else {
                                  updateProgress(-1, 100);
                                  updateMessage("正在呼叫...");
                              }
                          }
                          Thread.sleep(Integer.parseInt(settingModel.refreshTime.getValue()));
                          clearMp3File();
                          updateMessage("等待...");
                      }
                  } catch (SQLException | ClassNotFoundException throwables) {
                      logger.error(throwables.getMessage());
                      LOG.error(throwables.getMessage());
                      stop();
                      throwables.printStackTrace();
                  } catch (IOException ioException) {
                      logger.error("MP3生成失败!");
                      logger.error(ioException.getMessage());
                      LOG.error(ioException.getMessage());
                      stop();
                      ioException.printStackTrace();
                  } catch (Exception e) {
                      logger.error(e.getMessage());
                      LOG.error(e.getMessage());
                      stop();
                      e.printStackTrace();
                  }
                  //                catch (InterruptedException e) {
                  //                    logger.error("播放失败!");
                  //                    logger.error(e.getMessage());
                  //                    stop();
                  //                    e.printStackTrace();
                  //                }
                  return null;
              }
          };
      }
  }

  ```



  ##### JavaFx Log日志输出

  界面 :

  ![](/upload/files/png/16934fdbc283e2f23b5daa126ae7d12f.png)

  这个log日志的实现参考：https://stackoverflow.com/questions/24116858/most-efficient-way-to-log-messages-to-javafx-textarea-via-threads-with-simple-cu

  项目中使用了两个LOG，一个是GUI的LOG界面LogViewer，还有Log4j用来生成log文件。

  完毕。


title: C# DotNetty TCP对接 松下扣料机
intro: TCP Socket Server ， DotNetty实现。
created_time: 2021-11-27 06:00:02.054425
html_text:
  <h5 id="h5-tcp-socket-server-dotnetty-"><a name="TCP Socket Server ， DotNetty实现。" class="reference-link"></a><span class="header-link octicon octicon-link"></span>TCP Socket Server ， DotNetty实现。</h5><p>松下扣料机的厂商人员只来一天半，也就是要在一天内完成对接，你只能通过以往的报文日志来分析大致的通讯情况，松下厂商并不提供模拟测试程序。</p>
  <p>闭门造车写了几天，在正式对接那天血崩，意料之中。</p>
  <p>说下崩在哪。</p>
  <p>松下报文格式是这样的:</p>
  <p>心跳PING:</p>
  <p>(文档版)：</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113632493-1478398478.png" alt="">
  <p>你以为是什么？</p>
  <p>XML字符串吗？我是这么想的，但是对接那天我的程序始终无法和扣料机发生心跳，一发生心跳就断开连接，一群人在我电脑和扣料机跑来跑去看日志服了。</p>
  <p>报文有问题。</p>
  <p><code>&lt;STX&gt;</code>8<code>&lt;SHO&gt;</code>PING_REQ<code>&lt;ETX&gt;</code></p>
  <p>不是字符串，不是XML，而是ASCII码。</p>
  <pre><code class="lang-c#">        public static byte STX =  0x02;
          public static byte SOH = 0x01;
          public static byte ETX = 0x03;
          public static char STX_CHAR = '\u0002';
          public static char SHO_CHAR = '\u0001';
          public static char ETX_CHAR = '\u0003';
  </code></pre>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113648295-207426978.png" alt="">
  <p>将0X02对应字符粘贴到notepad++上。</p>
  <p><img src="https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113655871-1806930789.png" alt="">
  <p>无语了。</p>
  <p>然后改程序，算是勉强完成心跳，因为和Netty一样有自动弹射心跳所以不难。</p>
  <p>难得是解码器，在Socket通讯中要考虑以下情况。</p>
  <p>( tcp是流,没有界限.也就没所谓的包.这里包，算是约定)</p>
  <p>粘包、多包和少包, 断包</p>
  <pre><code class="lang-xml">&lt;message&gt;
   &lt;header messageClass="530" transactionID="1234567890" reply="0"&gt;
   &lt;location routeID="1001" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/&gt;
   &lt;/header&gt;
   &lt;body&gt;
   &lt;result errorCode="0" errorText="" actionCode=""/&gt;
  &lt;/body&gt;
  &lt;/message&gt;
  &lt;message&gt;
   &lt;header messageClass="530" transactionID="1234567890" reply="0"&gt;
   &lt;location routeID="1001" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/&gt;
   &lt;/header&gt;
   &lt;body&gt;
   &lt;result errorCode="0" errorText="" actionCode=""/&gt;
  &lt;/body&gt;
  &lt;/message&gt;
  </code></pre>
  <pre><code class="lang-xml">&lt;message&gt;
   &lt;header messageClass="530" transactionID="1234567890" reply="0"&gt;
   &lt;location routeID="10
  --第一次发送--
  01" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/&gt;
   &lt;/header&gt;
   &lt;body&gt;
   &lt;result errorCode="0" errorText="" actionCode=""/&gt;
  &lt;/body&gt;
  &lt;/message&gt;
  --第二次发送--
  </code></pre>
  <p>总之有许多情况要考虑，报文提供有长度，我开始也是通过长度去读取报文，但是我程序算出来的长度和扣料机发过来的报文长度是不一样的，不清楚是什么原因。</p>
  <p>最后不用它的长度，定义缓冲区，当有消息进来解码器while处理，将所有情况走一遍，而且如果有多个连接，能够保证在同时粘包断包的情况下互相不影响。</p>
  <pre><code class="lang-c#">  /// &lt;summary&gt;
      /// 解码
      /// &lt;/summary&gt;
      public class DecoderHandler : ByteToMessageDecoder
      {
          IByteBuffer byteBuffer = ByteBufferUtil.DefaultAllocator.Buffer();
          public Int32 PipleLineId = 0;

          protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List&lt;object&gt; output)
          {
              int len = input.ReadableBytes;
              byte[] bytes = new byte[len];
              input.GetBytes(0, bytes);
              SimpleLogHelper.LogInfo("PipleLineId = " + PipleLineId + "接收信息 : " + Encoding.Default.GetString(bytes));

              while (bytes.Length &gt; 0)
              {
               SimpleLogHelper.LogInfo("PipleLineId = " + PipleLineId + "本次处理 : " + Encoding.Default.GetString(bytes));
                  int stxIndex = Array.IndexOf(bytes, SocketHelper.STX);
                  int etxIndex = Array.IndexOf(bytes, SocketHelper.ETX);

                  //缓存没有
                  if (byteBuffer.ReadableBytes == 0)
                  {
                      //有头
                      if (stxIndex != -1)
                      {
                          //有尾
                          if (etxIndex != -1)
                          {
                              int shoIndex = Array.IndexOf(bytes, SocketHelper.SOH);
                              byte[] bodyLen = bytes.Skip(stxIndex + 1).Take(shoIndex - stxIndex - 1).ToArray();
                              byte[] message = bytes.Skip(shoIndex + 1).Take(etxIndex - shoIndex - 1 ).ToArray();
                              Package package = new Package();
                              package.length = int.Parse(Encoding.Default.GetString(bodyLen));
                              package.messageBody = Encoding.Default.GetString(message);
                              SocketHelper.SendPackage(package, output);
                              bytes = bytes.Skip(etxIndex + 1).ToArray();
                          }
                          else
                          {
                              //如果stxIndex 头部前有报文，丢弃前面的报文
                              byteBuffer.WriteBytes(bytes.Skip(stxIndex).ToArray());

                              int tmpLen = byteBuffer.ReadableBytes;
                              byte[] tmpBytes = new byte[tmpLen];
                              byteBuffer.GetBytes(0, tmpBytes);
                              SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存1 : " + Encoding.Default.GetString(tmpBytes));

                              bytes = new byte[0];
                          }
                      }
                      else
                      {
                          //缓存为空，报文没头，跳过
                          bytes = new byte[0];
                      }
                  }
                  else
                  {
                      if (stxIndex != -1)
                      {
                          if (stxIndex &lt; etxIndex || etxIndex == -1)
                          {
                              int tmpLen = byteBuffer.ReadableBytes;
                              byte[] tmpBytes = new byte[tmpLen];
                              byteBuffer.GetBytes(0, tmpBytes);
                              SimpleLogHelper.LogError("PipleLineId = " + PipleLineId + "丢弃 : " + Encoding.Default.GetString(tmpBytes));

                              bytes = bytes.Skip(stxIndex).ToArray();
                              byteBuffer.Clear();
                              continue;
                          }
                      }

                      if (etxIndex != -1)
                      {
                          byteBuffer.WriteBytes(bytes.Take(etxIndex + 1).ToArray());

                          int tmpLen = byteBuffer.ReadableBytes;
                          byte[] tmpBytes = new byte[tmpLen];
                          byteBuffer.GetBytes(0, tmpBytes);
                          SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存3 : " + Encoding.Default.GetString(tmpBytes));

                          int buffLen = byteBuffer.ReadableBytes;
                          byte[] currentBytes = new byte[buffLen];
                          byteBuffer.GetBytes(0, currentBytes);
                          int shoIndex = Array.IndexOf(currentBytes, SocketHelper.SOH);
                          byte[] bodyLen = currentBytes.Skip(1).Take(shoIndex - 1).ToArray();
                          byte[] message = currentBytes.Skip(shoIndex + 1).SkipLast(1).ToArray();

                          Package package = new Package();
                          package.length = int.Parse(Encoding.Default.GetString(bodyLen));
                          package.messageBody = Encoding.Default.GetString(message);
                          SocketHelper.SendPackage(package, output);

                          byteBuffer.Clear();
                          bytes = bytes.Skip(etxIndex + 1).ToArray();
                      }
                      else
                      {
                          //断包 进缓冲
                          byteBuffer.WriteBytes(bytes);
                          bytes = new byte[0];

                          int tmpLen = byteBuffer.ReadableBytes;
                          byte[] tmpBytes = new byte[tmpLen];
                          byteBuffer.GetBytes(0, tmpBytes);
                          SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存2 : " + Encoding.Default.GetString(tmpBytes));
                      }
                  }
              }
              input.SkipBytes(len);
          }
      }
  </code></pre>

markdown_text:
  ##### TCP Socket Server ， DotNetty实现。

  松下扣料机的厂商人员只来一天半，也就是要在一天内完成对接，你只能通过以往的报文日志来分析大致的通讯情况，松下厂商并不提供模拟测试程序。

  闭门造车写了几天，在正式对接那天血崩，意料之中。

  说下崩在哪。

  松下报文格式是这样的:

  心跳PING:

  (文档版)：

  ![](https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113632493-1478398478.png)


  你以为是什么？

  XML字符串吗？我是这么想的，但是对接那天我的程序始终无法和扣料机发生心跳，一发生心跳就断开连接，一群人在我电脑和扣料机跑来跑去看日志服了。

  报文有问题。

  `<STX>`8`<SHO>`PING_REQ`<ETX>`

  不是字符串，不是XML，而是ASCII码。

  ```c#
   	   public static byte STX =  0x02;
          public static byte SOH = 0x01;
          public static byte ETX = 0x03;
          public static char STX_CHAR = '\u0002';
          public static char SHO_CHAR = '\u0001';
          public static char ETX_CHAR = '\u0003';
  ```

  ![](https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113648295-207426978.png)




  将0X02对应字符粘贴到notepad++上。

  ![](https://img2020.cnblogs.com/blog/1252682/202102/1252682-20210211113655871-1806930789.png)

  无语了。

  然后改程序，算是勉强完成心跳，因为和Netty一样有自动弹射心跳所以不难。

  难得是解码器，在Socket通讯中要考虑以下情况。

  ( tcp是流,没有界限.也就没所谓的包.这里包，算是约定)

  粘包、多包和少包, 断包

  ```xml
  <message>
   <header messageClass="530" transactionID="1234567890" reply="0">
   <location routeID="1001" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/>
   </header>
   <body>
   <result errorCode="0" errorText="" actionCode=""/>
  </body>
  </message>
  <message>
   <header messageClass="530" transactionID="1234567890" reply="0">
   <location routeID="1001" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/>
   </header>
   <body>
   <result errorCode="0" errorText="" actionCode=""/>
  </body>
  </message>
  ```

  ```xml
  <message>
   <header messageClass="530" transactionID="1234567890" reply="0">
   <location routeID="10
  --第一次发送--
  01" routeName="LINE1" equipmentID="1000"
   equipmentName="equip1" zoneID="1001" zonePos="2" zoneName="SMT1" laneNo="0"
  controllerGuid="abcd12345678"/>
   </header>
   <body>
   <result errorCode="0" errorText="" actionCode=""/>
  </body>
  </message>
  --第二次发送--
  ```

  总之有许多情况要考虑，报文提供有长度，我开始也是通过长度去读取报文，但是我程序算出来的长度和扣料机发过来的报文长度是不一样的，不清楚是什么原因。

  最后不用它的长度，定义缓冲区，当有消息进来解码器while处理，将所有情况走一遍，而且如果有多个连接，能够保证在同时粘包断包的情况下互相不影响。

  ```c#
    /// <summary>
      /// 解码
      /// </summary>
      public class DecoderHandler : ByteToMessageDecoder
      {
          IByteBuffer byteBuffer = ByteBufferUtil.DefaultAllocator.Buffer();
          public Int32 PipleLineId = 0;

          protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
          {
              int len = input.ReadableBytes;
              byte[] bytes = new byte[len];
              input.GetBytes(0, bytes);
              SimpleLogHelper.LogInfo("PipleLineId = " + PipleLineId + "接收信息 : " + Encoding.Default.GetString(bytes));

              while (bytes.Length > 0)
              {
               SimpleLogHelper.LogInfo("PipleLineId = " + PipleLineId + "本次处理 : " + Encoding.Default.GetString(bytes));
                  int stxIndex = Array.IndexOf(bytes, SocketHelper.STX);
                  int etxIndex = Array.IndexOf(bytes, SocketHelper.ETX);

                  //缓存没有
                  if (byteBuffer.ReadableBytes == 0)
                  {
                      //有头
                      if (stxIndex != -1)
                      {
                          //有尾
                          if (etxIndex != -1)
                          {
                              int shoIndex = Array.IndexOf(bytes, SocketHelper.SOH);
                              byte[] bodyLen = bytes.Skip(stxIndex + 1).Take(shoIndex - stxIndex - 1).ToArray();
                              byte[] message = bytes.Skip(shoIndex + 1).Take(etxIndex - shoIndex - 1 ).ToArray();
                              Package package = new Package();
                              package.length = int.Parse(Encoding.Default.GetString(bodyLen));
                              package.messageBody = Encoding.Default.GetString(message);
                              SocketHelper.SendPackage(package, output);
                              bytes = bytes.Skip(etxIndex + 1).ToArray();
                          }
                          else
                          {
                              //如果stxIndex 头部前有报文，丢弃前面的报文
                              byteBuffer.WriteBytes(bytes.Skip(stxIndex).ToArray());

                              int tmpLen = byteBuffer.ReadableBytes;
                              byte[] tmpBytes = new byte[tmpLen];
                              byteBuffer.GetBytes(0, tmpBytes);
                              SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存1 : " + Encoding.Default.GetString(tmpBytes));

                              bytes = new byte[0];
                          }
                      }
                      else
                      {
                          //缓存为空，报文没头，跳过
                          bytes = new byte[0];
                      }
                  }
                  else
                  {
                      if (stxIndex != -1)
                      {
                          if (stxIndex < etxIndex || etxIndex == -1)
                          {
                              int tmpLen = byteBuffer.ReadableBytes;
                              byte[] tmpBytes = new byte[tmpLen];
                              byteBuffer.GetBytes(0, tmpBytes);
                              SimpleLogHelper.LogError("PipleLineId = " + PipleLineId + "丢弃 : " + Encoding.Default.GetString(tmpBytes));

                              bytes = bytes.Skip(stxIndex).ToArray();
                              byteBuffer.Clear();
                              continue;
                          }
                      }

                      if (etxIndex != -1)
                      {
                          byteBuffer.WriteBytes(bytes.Take(etxIndex + 1).ToArray());

                          int tmpLen = byteBuffer.ReadableBytes;
                          byte[] tmpBytes = new byte[tmpLen];
                          byteBuffer.GetBytes(0, tmpBytes);
                          SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存3 : " + Encoding.Default.GetString(tmpBytes));

                          int buffLen = byteBuffer.ReadableBytes;
                          byte[] currentBytes = new byte[buffLen];
                          byteBuffer.GetBytes(0, currentBytes);
                          int shoIndex = Array.IndexOf(currentBytes, SocketHelper.SOH);
                          byte[] bodyLen = currentBytes.Skip(1).Take(shoIndex - 1).ToArray();
                          byte[] message = currentBytes.Skip(shoIndex + 1).SkipLast(1).ToArray();

                          Package package = new Package();
                          package.length = int.Parse(Encoding.Default.GetString(bodyLen));
                          package.messageBody = Encoding.Default.GetString(message);
                          SocketHelper.SendPackage(package, output);

                          byteBuffer.Clear();
                          bytes = bytes.Skip(etxIndex + 1).ToArray();
                      }
                      else
                      {
                          //断包 进缓冲
                          byteBuffer.WriteBytes(bytes);
                          bytes = new byte[0];

                          int tmpLen = byteBuffer.ReadableBytes;
                          byte[] tmpBytes = new byte[tmpLen];
                          byteBuffer.GetBytes(0, tmpBytes);
                          SimpleLogHelper.LogInfoSuccess("PipleLineId = " + PipleLineId + "缓存2 : " + Encoding.Default.GetString(tmpBytes));
                      }
                  }
              }
              input.SkipBytes(len);
          }
      }
  ```


title: Qypt5 Excel 转 PDF 连接RabbitMQ自动打印
created_time: 2021-11-27 15:59:12.979020
intro:
  自动打印程序，Server端产生单子，Excel格式，将文件地址根据组发送到rabbitmq队列，程序订阅rabbitmq，抓取文件地址下载并调用打印机打印。 这里的订阅分组，每个组一个routing-key，程序可以自由订阅N个组。

html_text:
  <h2 id="h2-u4ECBu7ECD"><a name="介绍" class="reference-link"></a><span class="header-link octicon octicon-link"></span>介绍</h2><p>自动打印程序，Server端产生单子，Excel格式，将文件地址根据组发送到rabbitmq队列，程序订阅rabbitmq，抓取文件地址下载并调用打印机打印。<br>这里的订阅分组，每个组一个routing-key，程序可以自由订阅N个组。
  <p><img src="/upload/files/png/588e2c0d5ddce929c0bb6604e80ea750.png" alt="">
  <p>Python实现，GUI框架为Pyqt5。</p>
  <h4 id="h4-u8FDEu63A5u6253u5370u673A"><a name="连接打印机" class="reference-link"></a><span class="header-link octicon octicon-link"></span>连接打印机</h4><p>程序关闭会最小化到托盘，同时托盘拥有选择默认打印机功能。</p>
  <pre><code class="lang-python">
  class TrayIcon(QSystemTrayIcon):
      def __init__(self, parent=None, ):
          super(TrayIcon, self).__init__(parent)
          self.quit_action = QAction("退出", self, triggered=self.quit)
          self.default_printer = QAction('当前打印机:' + DEFAULT_PRINTER, self)
          self.refresh_action = QAction('刷新')
          self.refresh_action.triggered.connect(parent.init_data)
          self.printer_menus = QMenu()
          self.menu = QMenu()
          self.setToolTip("监控打印程序")

          self.show_menu()
          self.other()

      def show_menu(self):
          " 设计托盘的菜单，这里我实现了一个二级菜单 "
          print(DEFAULT_PRINTER)
          self.menu.addAction(self.default_printer)
          self.menu.addAction(self.refresh_action)
          self.menu.addSeparator()
          self.menu.addMenu(self.printer_menus, )

          self.menu.addAction(self.quit_action)
          self.printer_menus.setTitle("打印机列表")

          self.get_all_printer_actions()

          self.setContextMenu(self.menu)

      def get_all_printer_actions(self):
          for item in PRINTERS:
              action = QAction(item, self)
              action.triggered.connect(lambda: self.set_use_printer(self.sender()))
              if item == DEFAULT_PRINTER:
                  action.setCheckable(True)
                  action.setChecked(True)
              self.printer_menus.addAction(action)
              # self.printer_menus.addActions(printers)

      def set_use_printer(self, action):
          for item in self.printer_menus.actions():
              item.setCheckable(False)
              item.setChecked(False)
          action.setCheckable(True)
          action.setChecked(True)
          win32print.SetDefaultPrinterW(action.text())
          self.default_printer.setText('当前打印机:' + action.text())
          self.showMessage("信息", "当前打印机为:" + action.text(), self.icon)

      def other(self):
          self.activated.connect(self.iconClied)
          # 把鼠标点击图标的信号和槽连接
          # self.messageClicked.connect(self.mClied)
          # 把鼠标点击弹出消息的信号和槽连接
          self.setIcon(QIcon(":/images/logo.ico"))
          self.icon = self.MessageIcon()
          # 设置图标

      def iconClied(self, reason):
          "鼠标点击icon传递的信号会带有一个整形的值，1是表示单击右键，2是双击，3是单击左键，4是用鼠标中键点击"
          if reason == 2 or reason == 3:
              pw = self.parent()
              if pw.isVisible():
                  pw.hide()
              else:
                  pw.show()
          print(reason)


      def quit(self):
          "保险起见，为了完整的退出"
          self.setVisible(False)
          self.parent().exit()
          qApp.quit()
          sys.exit()
  </code></pre>
  <p>程序运行会获取所有本地打印机：</p>
  <pre><code class="lang-python">  if file.tryLock():  # 防止多开
          for it in win32print.EnumPrinters(2):
              PRINTERS.append(it[2])
          app.setQuitOnLastWindowClosed(False)
          w = window()
  </code></pre>
  <p><img src="/upload/files/png/9f9d1c93eef591be06da22c67942a8c9.png" alt="">
  <h4 id="h4-rabbitmq-"><a name="RabbitMQ 管理" class="reference-link"></a><span class="header-link octicon octicon-link"></span>RabbitMQ 管理</h4><p>RabbitMQ 订阅，取消订阅，文件下载，都在这了。</p>
  <pre><code class="lang-python">import logging
  import os
  import shutil
  import threading
  import time
  from urllib import request

  import pika
  import win32api
  import win32print
  import xlwings as xw

  logging.basicConfig(level=logging.DEBUG,  # 控制台打印的日志级别
                      filename='./printer.log',
                      filemode='a',
                      # a是追加模式，默认如果不写的话，就是追加模式
                      format=
                      '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                      # 日志格式
                      )

  import pythoncom

  EXCHANGE_NAME = 'exchange-printOrder'
  SAVE_PATH = os.path.abspath('.') + '\\download'
  LOCK = 0

  import PDF_Util


  class MQ(threading.Thread):
      def __init__(self, keys, window):
          super().__init__()
          credentials = pika.PlainCredentials('rabbitmq', 'P@ssw0rd')  # mq用户名和密码，没有则需要自己创建
          self.conn = pika.BlockingConnection(
              pika.ConnectionParameters(host='10.86.13.83', virtual_host='/', credentials=credentials))
          self.channel = self.conn.channel()
          self.current_keys = keys
          self.log_area = window.log_area
          self.log_area.append('初始化RabbitMQ 成功')
          self.window = window
          self.window.statusbar.showMessage('已连接')
          self.lock = False
          # threading.Thread(target=self.clear_file).start()
          self.clear_file()

      def clear_file(self):
          try:
              if not os.path.exists(SAVE_PATH):
                  os.mkdir(SAVE_PATH)
              else:
                  shutil.rmtree(SAVE_PATH)
                  os.mkdir(SAVE_PATH)
              logging.info('清空文件')
              print('clear')
          except PermissionError as p:
              print('文件被占用,wait')
              logging.info('文件被占用,wait')
              pass

      def refresh(self, key, flag):
          if flag == 0:
              self.channel.queue_unbind(self.queue_name, exchange=EXCHANGE_NAME, routing_key=key)
              print('解绑:', key)
              logging.info('解绑:' + key)
              self.log_area.append('解绑:' + key + '成功')
          elif flag == 1:
              self.channel.queue_bind(self.queue_name, exchange=EXCHANGE_NAME, routing_key=key)
              logging.info('绑定:' + key)
              print('绑定:', key)
              self.log_area.append('绑定:' + key + '成功')
          else:
              pass
          pass

      def call_back(self, ch, method, properties, body):
          print(ch, method, properties)
          self.lock = True

          logging.info('收到来自: %s 的消息: %s' % (method.routing_key, body))
          logging.info('下载来自: %s 的文件: %s' % (method.routing_key, body))
          self.log_area.append('下载来自: %s 的文件: %s' % (method.routing_key, body))
          if not os.path.exists(SAVE_PATH):
              os.makedirs(SAVE_PATH)
          self.download(str(body)[2:][0:-1])

      def get_file_name(self, url: str):
          return url[url.rindex('/') + 1:]

      def download(self, URL):
          try:
              req = request.Request(URL)
              res = request.urlopen(url=req, timeout=5)
              get_img = res.read()
              with open(SAVE_PATH + '\\' + self.get_file_name(URL), 'wb') as f:
                  f.write(get_img)
              f.close()
              self.window.ti.showMessage('提示', "打印: " + self.get_file_name(URL), self.window.ti.icon)
              self.log_area.append('文件: %s 下载成功，开始打印' % (self.get_file_name(URL)))
              logging.info('文件: %s 下载成功，开始打印' % (self.get_file_name(URL)))
              currentPrinter = win32print.GetDefaultPrinterW()
              # hinstance = win32api.ShellExecute(0, "print", SAVE_PATH + '\\' + self.get_file_name(URL),
              #                                   '/d:"%s"' % currentPrinter,
              #                                   None, 0)
              # logging.info('打印返回值 %s' % hinstance)

              # order = '{0} {1} {2}'.format(GS_PRINT, pdf, currentPrinter)
              # os.system(order)
              pythoncom.CoInitialize()
              pdf_util = PDF_Util.PDFConverter(SAVE_PATH + '\\' + self.get_file_name(URL))
              pdf_util.run_conver()

              self.log_area.append('打印完成')
              logging.info('打印完成')
              self.lock = False
          except Exception as e:
              logging.error(str(e))
              self.log_area.append(str(e))

      def run(self):
          self.channel.exchange_declare(EXCHANGE_NAME, 'topic', True)
          result = self.channel.queue_declare('', exclusive=True)
          self.queue_name = result.method.queue
          logging.info('队列名称:{0}'.format(self.queue_name))
          print('队列名称:{0}'.format(self.queue_name))
          self.log_area.append('当前队列名称:' + self.queue_name)

          # routing_key = 'printOrder[06-15-KIT-B-R-01]'
          for key in self.current_keys:
              self.channel.queue_bind(exchange=EXCHANGE_NAME, queue=self.queue_name, routing_key=key)
          self.channel.basic_consume(queue=self.queue_name, on_message_callback=self.call_back, auto_ack=True)
          currentPrinter = win32print.GetDefaultPrinterW()
          self.log_area.append('当前默认打印机:' + currentPrinter)
          self.channel.start_consuming()


  if __name__ == '__main__':
      mq = MQ(['printOrder[06-15-KIT-B-R-01]', 'printOrder[06-15-KIT-B-R-02]', 'printOrder[06-15-KIT-B-R-03]'])
      mq.start()
      mq.refresh(['printOrder[06-15-KIT-B-R-01]', 'printOrder[06-15-KIT-B-R-02]'])
  </code></pre>
  <p>我这里还做了本地持久化，会把订阅的信息存在C盘User下的.monitor文件夹的setting.ini里面，也就是程序关闭后，再次打开会自动订阅上次已订阅的组。</p>
  <pre><code class="lang-python">class Config(object):

      def __init__(self):
          self.conf = ConfigParser()
          self.path = os.path.expanduser('~') + '\\' + '.monitor'
          self.name = "setting.ini"
          self.dir = self.path + '\\' + self.name
          if not os.path.isdir(self.path):
              os.makedirs(self.path)
          if not os.path.isfile(self.dir):
              fd = open(self.dir, mode="w", encoding="utf-8")
              fd.close()
              self.init_data()

      # 初始化数据
      def init_data(self):
          self.conf.add_section("TOPIC")
          self.conf.set("TOPIC", "DATA", json.dumps(GROUPS))
          self.conf.write(open(self.dir, 'a'))

      def refresh(self):
          self.conf.remove_section('TOPIC')
          self.conf.add_section('TOPIC')
          self.conf.set("TOPIC", "DATA", json.dumps(GROUPS))
          self.conf.write(open(self.dir, 'w'))

      def get_groups(self):
          self.conf.read(self.dir)
          data = self.conf.get('TOPIC', 'DATA')
          arr = json.loads(data)
          return arr
  </code></pre>
  <h4 id="h4--excel"><a name="打印Excel" class="reference-link"></a><span class="header-link octicon octicon-link"></span>打印Excel</h4><p>我本以为打印Excel没什么难的，可是客户机器上的Excel版本都是2016，太老了，在打印Excel的时候是要调用Excel程序的，而Excel打印，然后还会弹出 ‘当前文件已被修改，XXXXXX’，这就很不友好，需要人去点，就做不到静默打印的效果，<br>这问题无解，我总不能去动Excel，最后憋急了，将Excel转PDF，再用第三方工具实现静默打印。
  <pre><code class="lang-python"># -*- coding:utf-8 -*-
  import logging
  import os
  import subprocess

  import win32api
  import win32print
  from win32com.client import Dispatch, constants, gencache, DispatchEx

  SAVE_PATH = os.path.abspath('.') + '\\download'
  # GS_WIN32 = r'C:\Program Files (x86)\gs\gs9.55.0\bin\gswin32.exe'
  GS_PRINT = os.path.abspath('.') + '\\PDFtoPrinter.exe'
  logging.basicConfig(level=logging.DEBUG,  # 控制台打印的日志级别
                      filename='./printer.log',
                      filemode='a',
                      # a是追加模式，默认如果不写的话，就是追加模式
                      format=
                      '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                      # 日志格式
                      )


  def print_pdf(export_file: str):
      currentPrinter = win32print.GetDefaultPrinterW()
      order = '{0} {1} {2}'.format(GS_PRINT, export_file, currentPrinter)
      res = subprocess.call(order, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      print('打印返回值{0}'.format(res))
      logging.info('打印返回值{0}'.format(res))

  class PDFConverter:
      def __init__(self, pathname, export='.'):
          self._handle_postfix = ['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx']
          self._filename_list = list()
          self._export_folder = SAVE_PATH
          if not os.path.exists(self._export_folder):
              os.mkdir(self._export_folder)
          self._enumerate_filename(pathname)

      def _enumerate_filename(self, pathname):
          '''
          读取所有文件名
          '''
          full_pathname = os.path.abspath(pathname)
          if os.path.isfile(full_pathname):
              if self._is_legal_postfix(full_pathname):
                  self._filename_list.append(full_pathname)
              else:
                  raise TypeError('文件 {} 后缀名不合法！仅支持如下文件类型：{}。'.format(pathname, '、'.join(self._handle_postfix)))
          elif os.path.isdir(full_pathname):
              for relpath, _, files in os.walk(full_pathname):
                  for name in files:
                      filename = os.path.join(full_pathname, relpath, name)
                      if self._is_legal_postfix(filename):
                          self._filename_list.append(os.path.join(filename))
          else:
              raise TypeError('文件/文件夹 {} 不存在或不合法！'.format(pathname))

      def _is_legal_postfix(self, filename):
          return filename.split('.')[-1].lower() in self._handle_postfix and not os.path.basename(filename).startswith(
              '~')

      def run_conver(self):
          '''
          进行批量处理，根据后缀名调用函数执行转换
          '''
          print('需要转换的文件数：', len(self._filename_list))
          for filename in self._filename_list:
              postfix = filename.split('.')[-1].lower()
              funcCall = getattr(self, postfix)
              print('原文件：', filename)
              funcCall(filename)
          print('转换完成！')

      def doc(self, filename):
          '''
          doc 和 docx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          print('保存 PDF 文件：', exportfile)
          gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
          w = Dispatch("Word.Application")
          doc = w.Documents.Open(filename)
          doc.ExportAsFixedFormat(exportfile, constants.wdExportFormatPDF,
                                  Item=constants.wdExportDocumentWithMarkup,
                                  CreateBookmarks=constants.wdExportCreateHeadingBookmarks)

          w.Quit(constants.wdDoNotSaveChanges)

      def docx(self, filename):
          self.doc(filename)

      def xls(self, filename):
          '''
          xls 和 xlsx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          xlApp = DispatchEx("Excel.Application")
          xlApp.Visible = False
          xlApp.DisplayAlerts = 0
          books = xlApp.Workbooks.Open(filename, False)
          books.ExportAsFixedFormat(0, exportfile)
          books.Close(False)
          print('保存 PDF 文件：', exportfile)
          print_pdf(exportfile)
          xlApp.Quit()

      def xlsx(self, filename):
          self.xls(filename)

      def ppt(self, filename):
          '''
          ppt 和 pptx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
          p = Dispatch("PowerPoint.Application")
          ppt = p.Presentations.Open(filename, False, False, False)
          ppt.ExportAsFixedFormat(exportfile, 2, PrintRange=None)
          print('保存 PDF 文件：', exportfile)
          p.Quit()

      def pptx(self, filename):
          self.ppt(filename)
  </code></pre>
  <pre><code class="lang-python">def print_pdf(export_file: str):
      currentPrinter = win32print.GetDefaultPrinterW()
      order = '{0} {1} {2}'.format(GS_PRINT, export_file, currentPrinter)
      res = subprocess.call(order, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      print('打印返回值{0}'.format(res))
      logging.info('打印返回值{0}'.format(res))
  </code></pre>
  <p>打印PDF需要借助PDFPrinter.exe，下载地址: <a href="http://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/AutoPrint/PDFtoPrinter.exe">http://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/AutoPrint/PDFtoPrinter.exe</a></p>

markdown_text:
  ## 介绍
  自动打印程序，Server端产生单子，Excel格式，将文件地址根据组发送到rabbitmq队列，程序订阅rabbitmq，抓取文件地址下载并调用打印机打印。
  这里的订阅分组，每个组一个routing-key，程序可以自由订阅N个组。

  ![](/upload/files/png/588e2c0d5ddce929c0bb6604e80ea750.png)

  Python实现，GUI框架为Pyqt5。

  #### 连接打印机
  程序关闭会最小化到托盘，同时托盘拥有选择默认打印机功能。
  ```python

  class TrayIcon(QSystemTrayIcon):
      def __init__(self, parent=None, ):
          super(TrayIcon, self).__init__(parent)
          self.quit_action = QAction("退出", self, triggered=self.quit)
          self.default_printer = QAction('当前打印机:' + DEFAULT_PRINTER, self)
          self.refresh_action = QAction('刷新')
          self.refresh_action.triggered.connect(parent.init_data)
          self.printer_menus = QMenu()
          self.menu = QMenu()
          self.setToolTip("监控打印程序")

          self.show_menu()
          self.other()

      def show_menu(self):
          " 设计托盘的菜单，这里我实现了一个二级菜单 "
          print(DEFAULT_PRINTER)
          self.menu.addAction(self.default_printer)
          self.menu.addAction(self.refresh_action)
          self.menu.addSeparator()
          self.menu.addMenu(self.printer_menus, )

          self.menu.addAction(self.quit_action)
          self.printer_menus.setTitle("打印机列表")

          self.get_all_printer_actions()

          self.setContextMenu(self.menu)

      def get_all_printer_actions(self):
          for item in PRINTERS:
              action = QAction(item, self)
              action.triggered.connect(lambda: self.set_use_printer(self.sender()))
              if item == DEFAULT_PRINTER:
                  action.setCheckable(True)
                  action.setChecked(True)
              self.printer_menus.addAction(action)
              # self.printer_menus.addActions(printers)

      def set_use_printer(self, action):
          for item in self.printer_menus.actions():
              item.setCheckable(False)
              item.setChecked(False)
          action.setCheckable(True)
          action.setChecked(True)
          win32print.SetDefaultPrinterW(action.text())
          self.default_printer.setText('当前打印机:' + action.text())
          self.showMessage("信息", "当前打印机为:" + action.text(), self.icon)

      def other(self):
          self.activated.connect(self.iconClied)
          # 把鼠标点击图标的信号和槽连接
          # self.messageClicked.connect(self.mClied)
          # 把鼠标点击弹出消息的信号和槽连接
          self.setIcon(QIcon(":/images/logo.ico"))
          self.icon = self.MessageIcon()
          # 设置图标

      def iconClied(self, reason):
          "鼠标点击icon传递的信号会带有一个整形的值，1是表示单击右键，2是双击，3是单击左键，4是用鼠标中键点击"
          if reason == 2 or reason == 3:
              pw = self.parent()
              if pw.isVisible():
                  pw.hide()
              else:
                  pw.show()
          print(reason)

     
      def quit(self):
          "保险起见，为了完整的退出"
          self.setVisible(False)
          self.parent().exit()
          qApp.quit()
          sys.exit()


  ```
  程序运行会获取所有本地打印机：
  ```python
    if file.tryLock():  # 防止多开
          for it in win32print.EnumPrinters(2):
              PRINTERS.append(it[2])
          app.setQuitOnLastWindowClosed(False)
          w = window()
  ```
  ![](/upload/files/png/9f9d1c93eef591be06da22c67942a8c9.png)

  #### RabbitMQ 管理

  RabbitMQ 订阅，取消订阅，文件下载，都在这了。
  ```python
  import logging
  import os
  import shutil
  import threading
  import time
  from urllib import request

  import pika
  import win32api
  import win32print
  import xlwings as xw

  logging.basicConfig(level=logging.DEBUG,  # 控制台打印的日志级别
                      filename='./printer.log',
                      filemode='a',
                      # a是追加模式，默认如果不写的话，就是追加模式
                      format=
                      '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                      # 日志格式
                      )

  import pythoncom

  EXCHANGE_NAME = 'exchange-printOrder'
  SAVE_PATH = os.path.abspath('.') + '\\download'
  LOCK = 0

  import PDF_Util


  class MQ(threading.Thread):
      def __init__(self, keys, window):
          super().__init__()
          credentials = pika.PlainCredentials('rabbitmq', 'P@ssw0rd')  # mq用户名和密码，没有则需要自己创建
          self.conn = pika.BlockingConnection(
              pika.ConnectionParameters(host='10.86.13.83', virtual_host='/', credentials=credentials))
          self.channel = self.conn.channel()
          self.current_keys = keys
          self.log_area = window.log_area
          self.log_area.append('初始化RabbitMQ 成功')
          self.window = window
          self.window.statusbar.showMessage('已连接')
          self.lock = False
          # threading.Thread(target=self.clear_file).start()
          self.clear_file()

      def clear_file(self):
          try:
              if not os.path.exists(SAVE_PATH):
                  os.mkdir(SAVE_PATH)
              else:
                  shutil.rmtree(SAVE_PATH)
                  os.mkdir(SAVE_PATH)
              logging.info('清空文件')
              print('clear')
          except PermissionError as p:
              print('文件被占用,wait')
              logging.info('文件被占用,wait')
              pass

      def refresh(self, key, flag):
          if flag == 0:
              self.channel.queue_unbind(self.queue_name, exchange=EXCHANGE_NAME, routing_key=key)
              print('解绑:', key)
              logging.info('解绑:' + key)
              self.log_area.append('解绑:' + key + '成功')
          elif flag == 1:
              self.channel.queue_bind(self.queue_name, exchange=EXCHANGE_NAME, routing_key=key)
              logging.info('绑定:' + key)
              print('绑定:', key)
              self.log_area.append('绑定:' + key + '成功')
          else:
              pass
          pass

      def call_back(self, ch, method, properties, body):
          print(ch, method, properties)
          self.lock = True

          logging.info('收到来自: %s 的消息: %s' % (method.routing_key, body))
          logging.info('下载来自: %s 的文件: %s' % (method.routing_key, body))
          self.log_area.append('下载来自: %s 的文件: %s' % (method.routing_key, body))
          if not os.path.exists(SAVE_PATH):
              os.makedirs(SAVE_PATH)
          self.download(str(body)[2:][0:-1])

      def get_file_name(self, url: str):
          return url[url.rindex('/') + 1:]

      def download(self, URL):
          try:
              req = request.Request(URL)
              res = request.urlopen(url=req, timeout=5)
              get_img = res.read()
              with open(SAVE_PATH + '\\' + self.get_file_name(URL), 'wb') as f:
                  f.write(get_img)
              f.close()
              self.window.ti.showMessage('提示', "打印: " + self.get_file_name(URL), self.window.ti.icon)
              self.log_area.append('文件: %s 下载成功，开始打印' % (self.get_file_name(URL)))
              logging.info('文件: %s 下载成功，开始打印' % (self.get_file_name(URL)))
              currentPrinter = win32print.GetDefaultPrinterW()
              # hinstance = win32api.ShellExecute(0, "print", SAVE_PATH + '\\' + self.get_file_name(URL),
              #                                   '/d:"%s"' % currentPrinter,
              #                                   None, 0)
              # logging.info('打印返回值 %s' % hinstance)

              # order = '{0} {1} {2}'.format(GS_PRINT, pdf, currentPrinter)
              # os.system(order)
              pythoncom.CoInitialize()
              pdf_util = PDF_Util.PDFConverter(SAVE_PATH + '\\' + self.get_file_name(URL))
              pdf_util.run_conver()

              self.log_area.append('打印完成')
              logging.info('打印完成')
              self.lock = False
          except Exception as e:
              logging.error(str(e))
              self.log_area.append(str(e))

      def run(self):
          self.channel.exchange_declare(EXCHANGE_NAME, 'topic', True)
          result = self.channel.queue_declare('', exclusive=True)
          self.queue_name = result.method.queue
          logging.info('队列名称:{0}'.format(self.queue_name))
          print('队列名称:{0}'.format(self.queue_name))
          self.log_area.append('当前队列名称:' + self.queue_name)

          # routing_key = 'printOrder[06-15-KIT-B-R-01]'
          for key in self.current_keys:
              self.channel.queue_bind(exchange=EXCHANGE_NAME, queue=self.queue_name, routing_key=key)
          self.channel.basic_consume(queue=self.queue_name, on_message_callback=self.call_back, auto_ack=True)
          currentPrinter = win32print.GetDefaultPrinterW()
          self.log_area.append('当前默认打印机:' + currentPrinter)
          self.channel.start_consuming()


  if __name__ == '__main__':
      mq = MQ(['printOrder[06-15-KIT-B-R-01]', 'printOrder[06-15-KIT-B-R-02]', 'printOrder[06-15-KIT-B-R-03]'])
      mq.start()
      mq.refresh(['printOrder[06-15-KIT-B-R-01]', 'printOrder[06-15-KIT-B-R-02]'])

  ```

  我这里还做了本地持久化，会把订阅的信息存在C盘User下的.monitor文件夹的setting.ini里面，也就是程序关闭后，再次打开会自动订阅上次已订阅的组。

  ```python
  class Config(object):

      def __init__(self):
          self.conf = ConfigParser()
          self.path = os.path.expanduser('~') + '\\' + '.monitor'
          self.name = "setting.ini"
          self.dir = self.path + '\\' + self.name
          if not os.path.isdir(self.path):
              os.makedirs(self.path)
          if not os.path.isfile(self.dir):
              fd = open(self.dir, mode="w", encoding="utf-8")
              fd.close()
              self.init_data()

      # 初始化数据
      def init_data(self):
          self.conf.add_section("TOPIC")
          self.conf.set("TOPIC", "DATA", json.dumps(GROUPS))
          self.conf.write(open(self.dir, 'a'))

      def refresh(self):
          self.conf.remove_section('TOPIC')
          self.conf.add_section('TOPIC')
          self.conf.set("TOPIC", "DATA", json.dumps(GROUPS))
          self.conf.write(open(self.dir, 'w'))

      def get_groups(self):
          self.conf.read(self.dir)
          data = self.conf.get('TOPIC', 'DATA')
          arr = json.loads(data)
          return arr

  ```

  #### 打印Excel
  我本以为打印Excel没什么难的，可是客户机器上的Excel版本都是2016，太老了，在打印Excel的时候是要调用Excel程序的，而Excel打印，然后还会弹出 '当前文件已被修改，XXXXXX'，这就很不友好，需要人去点，就做不到静默打印的效果，
  这问题无解，我总不能去动Excel，最后憋急了，将Excel转PDF，再用第三方工具实现静默打印。

  ```python
  # -*- coding:utf-8 -*-
  import logging
  import os
  import subprocess

  import win32api
  import win32print
  from win32com.client import Dispatch, constants, gencache, DispatchEx

  SAVE_PATH = os.path.abspath('.') + '\\download'
  # GS_WIN32 = r'C:\Program Files (x86)\gs\gs9.55.0\bin\gswin32.exe'
  GS_PRINT = os.path.abspath('.') + '\\PDFtoPrinter.exe'
  logging.basicConfig(level=logging.DEBUG,  # 控制台打印的日志级别
                      filename='./printer.log',
                      filemode='a',
                      # a是追加模式，默认如果不写的话，就是追加模式
                      format=
                      '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                      # 日志格式
                      )


  def print_pdf(export_file: str):
      currentPrinter = win32print.GetDefaultPrinterW()
      order = '{0} {1} {2}'.format(GS_PRINT, export_file, currentPrinter)
      res = subprocess.call(order, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      print('打印返回值{0}'.format(res))
      logging.info('打印返回值{0}'.format(res))

  class PDFConverter:
      def __init__(self, pathname, export='.'):
          self._handle_postfix = ['doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx']
          self._filename_list = list()
          self._export_folder = SAVE_PATH
          if not os.path.exists(self._export_folder):
              os.mkdir(self._export_folder)
          self._enumerate_filename(pathname)

      def _enumerate_filename(self, pathname):
          '''
          读取所有文件名
          '''
          full_pathname = os.path.abspath(pathname)
          if os.path.isfile(full_pathname):
              if self._is_legal_postfix(full_pathname):
                  self._filename_list.append(full_pathname)
              else:
                  raise TypeError('文件 {} 后缀名不合法！仅支持如下文件类型：{}。'.format(pathname, '、'.join(self._handle_postfix)))
          elif os.path.isdir(full_pathname):
              for relpath, _, files in os.walk(full_pathname):
                  for name in files:
                      filename = os.path.join(full_pathname, relpath, name)
                      if self._is_legal_postfix(filename):
                          self._filename_list.append(os.path.join(filename))
          else:
              raise TypeError('文件/文件夹 {} 不存在或不合法！'.format(pathname))

      def _is_legal_postfix(self, filename):
          return filename.split('.')[-1].lower() in self._handle_postfix and not os.path.basename(filename).startswith(
              '~')

      def run_conver(self):
          '''
          进行批量处理，根据后缀名调用函数执行转换
          '''
          print('需要转换的文件数：', len(self._filename_list))
          for filename in self._filename_list:
              postfix = filename.split('.')[-1].lower()
              funcCall = getattr(self, postfix)
              print('原文件：', filename)
              funcCall(filename)
          print('转换完成！')

      def doc(self, filename):
          '''
          doc 和 docx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          print('保存 PDF 文件：', exportfile)
          gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
          w = Dispatch("Word.Application")
          doc = w.Documents.Open(filename)
          doc.ExportAsFixedFormat(exportfile, constants.wdExportFormatPDF,
                                  Item=constants.wdExportDocumentWithMarkup,
                                  CreateBookmarks=constants.wdExportCreateHeadingBookmarks)

          w.Quit(constants.wdDoNotSaveChanges)

      def docx(self, filename):
          self.doc(filename)

      def xls(self, filename):
          '''
          xls 和 xlsx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          xlApp = DispatchEx("Excel.Application")
          xlApp.Visible = False
          xlApp.DisplayAlerts = 0
          books = xlApp.Workbooks.Open(filename, False)
          books.ExportAsFixedFormat(0, exportfile)
          books.Close(False)
          print('保存 PDF 文件：', exportfile)
          print_pdf(exportfile)
          xlApp.Quit()

      def xlsx(self, filename):
          self.xls(filename)

      def ppt(self, filename):
          '''
          ppt 和 pptx 文件转换
          '''
          name = os.path.basename(filename).split('.')[0] + '.pdf'
          exportfile = os.path.join(self._export_folder, name)
          gencache.EnsureModule('{00020905-0000-0000-C000-000000000046}', 0, 8, 4)
          p = Dispatch("PowerPoint.Application")
          ppt = p.Presentations.Open(filename, False, False, False)
          ppt.ExportAsFixedFormat(exportfile, 2, PrintRange=None)
          print('保存 PDF 文件：', exportfile)
          p.Quit()

      def pptx(self, filename):
          self.ppt(filename)

  ```

  ```python
  def print_pdf(export_file: str):
      currentPrinter = win32print.GetDefaultPrinterW()
      order = '{0} {1} {2}'.format(GS_PRINT, export_file, currentPrinter)
      res = subprocess.call(order, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
      print('打印返回值{0}'.format(res))
      logging.info('打印返回值{0}'.format(res))

  ```
  打印PDF需要借助PDFPrinter.exe，下载地址: http://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/AutoPrint/PDFtoPrinter.exe


title: 树莓派简单NAS配置和SSH内网穿透
intro: 树莓派简单NAS配置和SSH内网穿透
created_time: 2021-12-08 13:25:58.211741
html_text:
  <h4 id="h4--nas-nginx-ssh-nas-nginx-ssh"><a name="树莓派搭建NAS Nginx配置 内网穿透开始远程SSH树莓派搭建NAS Nginx配置 内网穿透开始远程SSH" class="reference-link"></a><span class="header-link octicon octicon-link"></span>树莓派搭建NAS Nginx配置 内网穿透开始远程SSH树莓派搭建NAS Nginx配置 内网穿透开始远程SSH</h4><h5 id="h5--nas"><a name="树莓派搭建NAS" class="reference-link"></a><span class="header-link octicon octicon-link"></span>树莓派搭建NAS</h5><p>我有一个移动硬盘，将移动硬盘路径通过Nginx映射出去，就可以得到一个简单的文件访问网页。</p>
  <p>Nginx 配置文件说明:</p>
  <pre><code>    server {
              listen       8080;
              listen       [::]:8080;
              server_name  192.168.0.200:8080;
              root         /usr/share/nginx/html;
          charset utf-8;
              # Load configuration files for the default server block.
          location / {
              if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
                  add_header Content-Disposition attachment;
              }            
              autoindex on;
                      alias /media/pi/disk/; 

          }
          location ^~/学习资料/{
              if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
                  add_header Content-Disposition attachment;
              }    
              auth_basic TEST-Login;
              auth_basic_user_file /etc/nginx/htpfile;
              autoindex on;
              autoindex_exact_size on;
              autoindex_localtime on;
              alias /media/pi/disk/学习资料/;
          }
              error_page 404 /404.html;
                      location = /40x.html {
              }

              error_page 500 502 503 504 /50x.html;
                      location = /50x.html {
              }
          }
  </code></pre><p>这里的 alias /media/pi/disk/ 表示将/media/pi/disk/映射出去。<br>autoindex on; 表示开启目录列表功能。
  <pre><code>  location / {
              if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
                  add_header Content-Disposition attachment;
              }            
              autoindex on;
                      alias /media/pi/disk/; 
          }
  </code></pre><p>然后就可以得到这样的界面。<br><img src="/upload/files/png/e46f85bb77cb4f3cf4da6f749aa65ed6.png" alt="">
  <p>这里还有一个配置，就是学习资料下的目录我不想让任何人都可以看到，我想加上一个认证拦截，用Nginx的内置模块就可以实现。</p>
  <pre><code>    location ^~/学习资料/{
              if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
                  add_header Content-Disposition attachment;
              }    
              auth_basic TEST-Login;
              auth_basic_user_file /etc/nginx/htpfile;
              autoindex on;
              autoindex_exact_size on;
              autoindex_localtime on;
              alias /media/pi/disk/学习资料/;
          }
  </code></pre><p>之后再打开学习资料这个文件夹就会弹出验证框:<br><img src="/upload/files/png/65620b5b7b4411da3d614b30d98bb1f0.png" alt=""><br>当路由匹配到学习资料会执行一个逻辑，request_filename 如果是 txt 等文件类型不用浏览器打开，直接下载。<br>auth_basic TEST-Login;<br>auth_basic_user_file /etc/nginx/htpfile;<br>表示开始登录拦截，并指定用户名密码的文件。<br>用户名密码的文件可以用 apache-tools  htpasswd 来创建，密码是加密的。<br>    yangchaojie:{SHA}PiEmk9fh/PS+T+DymG/I/2s9aBM=<br>格式为，用户名:密码。
  <h6 id="h6--22-ssh"><a name="内网穿透22端口远程SSH" class="reference-link"></a><span class="header-link octicon octicon-link"></span>内网穿透22端口远程SSH</h6><p>首先开启ssh。</p>
  <pre><code class="lang-bash">sudo /etc/init.d/ssh start
  </code></pre>
  <p>然后防火墙开启22端口。</p>
  <pre><code class="lang-bash">ufw allow 22
  </code></pre>
  <p>然后映射出去就可以用ip:端口方式远程控制树莓派了。</p>

markdown_text:
  #### 树莓派搭建NAS Nginx配置 内网穿透开始远程SSH树莓派搭建NAS Nginx配置 内网穿透开始远程SSH

  ##### 树莓派搭建NAS

  我有一个移动硬盘，将移动硬盘路径通过Nginx映射出去，就可以得到一个简单的文件访问网页。

  Nginx 配置文件说明:

  ```
  	server {
          	listen       8080;
          	listen       [::]:8080;
          	server_name  192.168.0.200:8080;
          	root         /usr/share/nginx/html;
  		charset utf-8;
          	# Load configuration files for the default server block.
  		location / {
  			if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
  				add_header Content-Disposition attachment;
  			}			
  			autoindex on;
              		alias /media/pi/disk/; 
  				
  		}
  		location ^~/学习资料/{
  			if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
  				add_header Content-Disposition attachment;
  			}	
  			auth_basic TEST-Login;
  			auth_basic_user_file /etc/nginx/htpfile;
  			autoindex on;
  			autoindex_exact_size on;
  			autoindex_localtime on;
  			alias /media/pi/disk/学习资料/;
  		}
          	error_page 404 /404.html;
              		location = /40x.html {
          	}

          	error_page 500 502 503 504 /50x.html;
              		location = /50x.html {
          	}
      	}

  ```


  这里的 alias /media/pi/disk/ 表示将/media/pi/disk/映射出去。
  autoindex on; 表示开启目录列表功能。
  ```
    location / {
              if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
                  add_header Content-Disposition attachment;
              }            
              autoindex on;
                      alias /media/pi/disk/; 
          }
  ```

  然后就可以得到这样的界面。
  ![](/upload/files/png/e46f85bb77cb4f3cf4da6f749aa65ed6.png)

  这里还有一个配置，就是学习资料下的目录我不想让任何人都可以看到，我想加上一个认证拦截，用Nginx的内置模块就可以实现。
  ```
  	location ^~/学习资料/{
  			if ($request_filename ~* ^.*?\.(txt|pdf|doc|xls)$){
  				add_header Content-Disposition attachment;
  			}	
  			auth_basic TEST-Login;
  			auth_basic_user_file /etc/nginx/htpfile;
  			autoindex on;
  			autoindex_exact_size on;
  			autoindex_localtime on;
  			alias /media/pi/disk/学习资料/;
  		}
  ```

  之后再打开学习资料这个文件夹就会弹出验证框:
  ![](/upload/files/png/65620b5b7b4411da3d614b30d98bb1f0.png)
  当路由匹配到学习资料会执行一个逻辑，request_filename 如果是 txt 等文件类型不用浏览器打开，直接下载。
  auth_basic TEST-Login;
  auth_basic_user_file /etc/nginx/htpfile;
  表示开始登录拦截，并指定用户名密码的文件。
  用户名密码的文件可以用 apache-tools  htpasswd 来创建，密码是加密的。
      yangchaojie:{SHA}PiEmk9fh/PS+T+DymG/I/2s9aBM=
  格式为，用户名:密码。

  ###### 内网穿透22端口远程SSH

  首先开启ssh。
  ```bash
  sudo /etc/init.d/ssh start
  ```
  然后防火墙开启22端口。
  ```bash
  ufw allow 22
  ```

  然后映射出去就可以用ip:端口方式远程控制树莓派了。


title: Oblivion Construction Set lesson 1
intro: Oblivion MOD 制作教程：CSE基本操作和基本概念。
created_time: 2021-12-24 18:09:51.579562
html_text:
  <h5 id="h5-u9700u6C42"><a name="需求" class="reference-link"></a><span class="header-link octicon octicon-link"></span>需求</h5><p>上古卷轴4 本体 (我用的是steam)版本</p>
  <p><a href="http://s3.z100.vip:28592/oblivion/CSE/CSE-36370-11-0-1634149099.7z">Construction Set Extender</a>  构造集 制作 MOD 使用 目测是 delphi 写的，我将CSE上传到了我的NAS，可以点击链接快速下载(2MB/S) </p>
  <p><a href="http://s3.z100.vip:28592/oblivion/OBSE/xOBSE22.5.zip"><strong>Oblivion Script Extender</strong></a> (OBSE) 不知道是什么东西，但似乎很重要，我认为这事为MOD和脚本提供了基本的运行环境的一个东西，这样的话这玩意就挺牛逼的。</p>
  <p>然后可能需要一个汉化补丁：<a href="http://s3.z100.vip:28592/oblivion/zh_CN/bd188198.zip">下载地址</a></p>
  <p>CSE 和 OBSE 只要直接解压到游戏目录就可以了，汉化补丁也是。</p>
  <p>如果你遇到 <code>由于找不到 SSCE5432.dll，无法继续执行代码。重新安装程序可能会解决此问题。</code></p>
  <p>请下载 <a href="http://s3.z100.vip:28592/oblivion/requirement/DLL/ssce5432.zip">SSCE5432.dll</a> 至游戏根目录。</p>
  <h5 id="h5--cse-"><a name="熟悉CSE的基本操作" class="reference-link"></a><span class="header-link octicon octicon-link"></span>熟悉CSE的基本操作</h5><p>首先打开游戏目录下的 <code>TESConstructionSet.exe</code> ，你大概会得到一个这样的界面。<br><img src="/upload/files/png/2e23220684e7d3d9e162f79fafceaf68.png" alt="">
  <p>有点懵，不知道如何开始？(我也是根据官方教程来的，不过相对看官方有些啰嗦的英文文档，这些都是我总结的最精简易懂的过程)</p>
  <h5 id="h5-u52A0u8F7Du6E38u620Fu672Cu4F53"><a name="加载游戏本体" class="reference-link"></a><span class="header-link octicon octicon-link"></span>加载游戏本体</h5><p>可以看到CSE刚打开Cell View 下是没有任何内容的，Cell View 是包含了游戏内的各种场景，那么就需要加载游戏本体，将游戏本体的内容通过CSE读取出来。</p>
  <p>点击 File -&gt; data  -&gt; 勾选 Oblivion.esm  -&gt; OK</p>
  <p><img src="/upload/files/png/23142cd9aa6c8007da4dd4e42ebbd83b.png" alt="">
  <p>数据很多，需要加载一会。</p>
  <p>加载完成，游戏本体的场景就会在Cell View中出现。</p>
  <p><img src="/upload/files/png/52e3f6dcc1ceec7683b759720a608ffd.png" alt="">
  <h5 id="h5-u505Au4E00u4E2Au5927u9152u676F"><a name="做一个大酒杯" class="reference-link"></a><span class="header-link octicon octicon-link"></span>做一个大酒杯</h5><p>在Oblivion帝国左边有个酒店，英文名叫做 Wawnetinn Tavern 可以在Cell View 的左侧菜单栏中找到，双击打开。</p>
  <p><img src="/upload/files/png/5273155854f6b74c3a4d44e69c2ad0da.png" alt="">
  <p>接下来就是开始熟悉相机操作，你将通过键鼠来对3D模型进行观察，以下是快捷键操作。</p>
  <blockquote>
  <pre><code>中心滚轮按住 + 鼠标 - 平移
  空格键 + 鼠标 - 平移
  Shift + 鼠标 - 旋转
  中心滚轮 滚动 - 缩放，步进
  V + 鼠标 - 缩放，精细
  C - 中心在选定的
  T - 顶视图选定的对象
  LMB + 鼠标 - 在 x 上移动和 y 轴
  Z + LMB + 鼠标 - 在 z 轴上移动
  X + LMB + 鼠标 - 在 x 轴上移动
  Y + LMB + 鼠标 - 在 y 轴上移动
  S + 鼠标 - 缩放大小
  RMB + 鼠标 - 绕 z 轴
  Y + RMB旋转+ 鼠标 - 绕 y 轴旋转
  X + RMB + 鼠标 - 绕 x 轴旋转
  D – 取消选择
  F – 落下
  CTRL x – 剪切
  CTRL c – 复制
  CTRL v – 粘贴


  CTRL d – 在当前位置复制和粘贴（重复）
  LMB（不选择任何内容）+ frame – 选择多对象
  CTRL + LMB – 添加到选择列表Misc 
  A – Illuminate（切换亮光）
  </code></pre></blockquote>
  <p>一开始渲染界面的3D模型视角很小，而且是TOP面，如果我们想将视角移动要酒馆内，就需要进行一些操作。</p>
  <p>一个快捷进入酒馆内视角的过程： 鼠标单击酒馆按T  看到酒馆的TOP面，然后按X的，进行X轴移动，然后按住SHIFT加鼠标左键将酒馆从TOP面移动成侧正面。</p>
  <p><img src="/upload/files/png/bddaf1f7298d4baa9abfeea4e3950c42.png" alt="">
  <p>然后滚轮进行放大，配合空格键，T键，可以轻松进入酒馆吧台。</p>
  <p><img src="/upload/files/png/67577f86f2f7a888c875b09581863626.png" alt="">
  <p>做一个很大的杯子。</p>
  <p><img src="/upload/files/png/eb904e307ac0752ab5d1262e52afb34f.png" alt="">
  <p>使用鼠标将旁边柜子上的杯子拖到吧台，按F下落。</p>
  <p>然后按S进行放大和缩小，当然有更快捷的方法，双击酒杯打开对话框。</p>
  <p><img src="/upload/files/png/9c1a3da0f63b1feeb489031f2e7d0c27.png" alt="">
  <p>将Scale改为5你就能得到一个很大的杯子。</p>
  <p><img src="/upload/files/png/ce0284aaf3ee7f8cface031152d81acb.png" alt="">
  <p>好了，你可以改个名字，比如将这个酒杯的名字改为：“杨超杰用过的杯子”</p>
  <p>在杯子的属性对话框中点击EDIT BASE 修改基础信息。</p>
  <p>修改name。</p>
  <p><img src="/upload/files/png/44c4adc559aca721bded78518222cde9.png" alt="">
  <p>好，点击OK，将焦点在渲染窗口，Ctrl+S保存修改，或File -&gt; Save。</p>
  <p><img src="/upload/files/png/9d2aab92dcc1de143fef21161cdc4434.png" alt="">
  <p>保存至Data，然后将该ESP文件生效。</p>
  <p><img src="/upload/files/png/65be0d97e42612dce8c057292c803863.png" alt="">
  <p>进入游戏。</p>
  <p><img src="/upload/files/png/e6f7472a2be06d3b8d49584cc7c876d7.png" alt="">
  <p><img src="/upload/files/png/4e682a6248d5980cf00c73e8217e9145.png" alt="">
  <p>一个简单的MOD就做好了。</p>
  <h6 id="h6-u8865u5145"><a name="补充" class="reference-link"></a><span class="header-link octicon octicon-link"></span>补充</h6><p>地图快速检索网站:  <a href="https://tamrielma.ps/">https://tamrielma.ps/</a></p>
  <h5 id="h5-u53C2u8003"><a name="参考" class="reference-link"></a><span class="header-link octicon octicon-link"></span>参考</h5><p><a href="https://wiki.nexusmods.com/index.php/How_to_make_Oblivion_mods">如何制作 Oblivion 模组</a></p>
  <p><a href="https://cs.elderscrolls.com/index.php?title=A_beginner%27s_guide,_lesson_1_-_The_Construction_Set_Primer">初学者指南，第 1 课 - 构造集入门</a></p>

markdown_text:
  ##### 需求

  上古卷轴4 本体 (我用的是steam)版本

  [Construction Set Extender](http://s3.z100.vip:28592/oblivion/CSE/CSE-36370-11-0-1634149099.7z)  构造集 制作 MOD 使用 目测是 delphi 写的，我将CSE上传到了我的NAS，可以点击链接快速下载(2MB/S) 

  [**Oblivion Script Extender**](http://s3.z100.vip:28592/oblivion/OBSE/xOBSE22.5.zip) (OBSE) 不知道是什么东西，但似乎很重要，我认为这事为MOD和脚本提供了基本的运行环境的一个东西，这样的话这玩意就挺牛逼的。

  然后可能需要一个汉化补丁：[下载地址](http://s3.z100.vip:28592/oblivion/zh_CN/bd188198.zip)

  CSE 和 OBSE 只要直接解压到游戏目录就可以了，汉化补丁也是。

  如果你遇到 `由于找不到 SSCE5432.dll，无法继续执行代码。重新安装程序可能会解决此问题。 `

  请下载 [SSCE5432.dll](http://s3.z100.vip:28592/oblivion/requirement/DLL/ssce5432.zip) 至游戏根目录。

  ##### 熟悉CSE的基本操作

  首先打开游戏目录下的 `TESConstructionSet.exe` ，你大概会得到一个这样的界面。
  ![](/upload/files/png/2e23220684e7d3d9e162f79fafceaf68.png)

  有点懵，不知道如何开始？(我也是根据官方教程来的，不过相对看官方有些啰嗦的英文文档，这些都是我总结的最精简易懂的过程)

  ##### 加载游戏本体

  可以看到CSE刚打开Cell View 下是没有任何内容的，Cell View 是包含了游戏内的各种场景，那么就需要加载游戏本体，将游戏本体的内容通过CSE读取出来。

  点击 File -> data  -> 勾选 Oblivion.esm  -> OK

  ![](/upload/files/png/23142cd9aa6c8007da4dd4e42ebbd83b.png)

  数据很多，需要加载一会。

  加载完成，游戏本体的场景就会在Cell View中出现。

  ![](/upload/files/png/52e3f6dcc1ceec7683b759720a608ffd.png)


  ##### 做一个大酒杯

  在Oblivion帝国左边有个酒店，英文名叫做 Wawnetinn Tavern 可以在Cell View 的左侧菜单栏中找到，双击打开。

  ![](/upload/files/png/5273155854f6b74c3a4d44e69c2ad0da.png)

  接下来就是开始熟悉相机操作，你将通过键鼠来对3D模型进行观察，以下是快捷键操作。

  > ```
  > 中心滚轮按住 + 鼠标 - 平移
  > 空格键 + 鼠标 - 平移
  > Shift + 鼠标 - 旋转
  > 中心滚轮 滚动 - 缩放，步进
  > V + 鼠标 - 缩放，精细
  > C - 中心在选定的
  > T - 顶视图选定的对象
  > LMB + 鼠标 - 在 x 上移动和 y 轴
  > Z + LMB + 鼠标 - 在 z 轴上移动
  > X + LMB + 鼠标 - 在 x 轴上移动
  > Y + LMB + 鼠标 - 在 y 轴上移动
  > S + 鼠标 - 缩放大小
  > RMB + 鼠标 - 绕 z 轴
  > Y + RMB旋转+ 鼠标 - 绕 y 轴旋转
  > X + RMB + 鼠标 - 绕 x 轴旋转
  > D – 取消选择
  > F – 落下
  > CTRL x – 剪切
  > CTRL c – 复制
  > CTRL v – 粘贴
  > 
  > 
  > CTRL d – 在当前位置复制和粘贴（重复）
  > LMB（不选择任何内容）+ frame – 选择多对象
  > CTRL + LMB – 添加到选择列表Misc 
  > A – Illuminate（切换亮光）
  > ```

  一开始渲染界面的3D模型视角很小，而且是TOP面，如果我们想将视角移动要酒馆内，就需要进行一些操作。

  一个快捷进入酒馆内视角的过程： 鼠标单击酒馆按T  看到酒馆的TOP面，然后按X的，进行X轴移动，然后按住SHIFT加鼠标左键将酒馆从TOP面移动成侧正面。

  ![](/upload/files/png/bddaf1f7298d4baa9abfeea4e3950c42.png)

  然后滚轮进行放大，配合空格键，T键，可以轻松进入酒馆吧台。

  ![](/upload/files/png/67577f86f2f7a888c875b09581863626.png)

  做一个很大的杯子。

  ![](/upload/files/png/eb904e307ac0752ab5d1262e52afb34f.png)

  使用鼠标将旁边柜子上的杯子拖到吧台，按F下落。

  然后按S进行放大和缩小，当然有更快捷的方法，双击酒杯打开对话框。

  ![](/upload/files/png/9c1a3da0f63b1feeb489031f2e7d0c27.png)


  将Scale改为5你就能得到一个很大的杯子。

  ![](/upload/files/png/ce0284aaf3ee7f8cface031152d81acb.png)

  好了，你可以改个名字，比如将这个酒杯的名字改为：“杨超杰用过的杯子”

  在杯子的属性对话框中点击EDIT BASE 修改基础信息。

  修改name。

  ![](/upload/files/png/44c4adc559aca721bded78518222cde9.png)

  好，点击OK，将焦点在渲染窗口，Ctrl+S保存修改，或File -> Save。

  ![](/upload/files/png/9d2aab92dcc1de143fef21161cdc4434.png)

  保存至Data，然后将该ESP文件生效。

  ![](/upload/files/png/65be0d97e42612dce8c057292c803863.png)

  进入游戏。

  ![](/upload/files/png/e6f7472a2be06d3b8d49584cc7c876d7.png)

  ![](/upload/files/png/4e682a6248d5980cf00c73e8217e9145.png)

  一个简单的MOD就做好了。

  ###### 补充

  地图快速检索网站:  https://tamrielma.ps/

  ##### 参考

  [如何制作 Oblivion 模组](https://wiki.nexusmods.com/index.php/How_to_make_Oblivion_mods)

  [初学者指南，第 1 课 - 构造集入门](https://cs.elderscrolls.com/index.php?title=A_beginner%27s_guide,_lesson_1_-_The_Construction_Set_Primer)


title: Vuetify封装通用查询和表单
created_time: 2021-12-30 12:18:42.216906
intro:
  在使用Vuetify做前台的时候，大量CRUD的功能十分繁琐，而且会让VUE代码变得混乱难以维护，那么对CRUD的组件化封装必不可少。  首先需要一个JSON数据结构来构建界面，通过参数化配置，可以灵活生成表单。  我封装的表单支持，text，select，AutoComplate(可以请求，异步查询)，checkbox，number

html_text:
  <h4 id="h4-u524Du8A00"><a name="前言" class="reference-link"></a><span class="header-link octicon octicon-link"></span>前言</h4><p>在使用Vuetify做前台的时候，大量CRUD的功能十分繁琐，而且会让VUE代码变得混乱难以维护，那么对CRUD的组件化封装必不可少。</p>
  <p>首先需要一个JSON数据结构来构建界面，通过参数化配置，可以灵活生成表单。</p>
  <p>我封装的表单支持，text，select，AutoComplate(可以请求，异步查询)，checkbox，number</p>
  <h4 id="h4-example"><a name="Example" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Example</h4><pre><code class="lang-json">  formModel: [
      {
        code: 'Code',
        name: '区域代码',
        formType: 'text', //表单类型  text，select，AutoComplate，checkbox，number
        value: '',
        notNull: true, //是否为空
        col: 6, //布局
        readonly: false //是否只读
      },
      {
        code: 'Name',
        name: '区域名称',
        formType: 'text',
        value: '',
        notNull: true,
        col: 6
      },
      {
        name: '工厂',
        code: 'Plant',
        value: '',
        formType: 'autocomplete',
        loading: false, //加载
        searchText: null, //查询文本
        selectText: 'Name',//参数对应
        selectValue: 'Code',//参数对应
        icon: 'mdi-factory',//查询图标
        requestModel: { //请求参数构建
          url: '/Plant/Search', 
          params: {
            code: '',
            name: ''
          },
          activeParam: 'name', //在查询时，只有一个查询条件会变化，也就是autocomplates组件在查询时会将searchText赋值给params的name，code不会变
          methods: 'post'
        },
        notNull: true
      },
      {
        code: 'Version',
        name: '版本',
        formType: 'number',
        value: 1,
        notNull: true,
        col: 6
      },
      {
        code: 'IsActive',
        name: '生效',
        formType: 'check',
        value: true,
        notNull: true,
        col: 6
      }
    ]
  </code></pre>
  <p>会生成以下表单：</p>
  <p><img src="/upload/files/png/6a6bf79d22dd5573cf47a740ac158851.png" alt="">
  <p>修改可以通过从表格获取item数据，在将值赋给formModel，同时在内部完成接口请求操作。</p>
  <p>CommonForm.vue 源码</p>
  <pre><code class="lang-vue">&lt;!-- shit mountain. Don't touch.  Just Use--&gt;
  &lt;template&gt;
    &lt;v-row justify="center"&gt;
      &lt;v-dialog
        v-model="dialog"
        persistent
        max-width="600px"
      &gt;
        &lt;!--      &lt;template v-slot:activator="{ on, attrs }"&gt;--&gt;
        &lt;!--        &lt;v-btn--&gt;
        &lt;!--          color="primary"--&gt;
        &lt;!--          dark--&gt;
        &lt;!--          v-bind="attrs"--&gt;
        &lt;!--          v-on="on"--&gt;
        &lt;!--        &gt;--&gt;
        &lt;!--          Open Dialog--&gt;
        &lt;!--        &lt;/v-btn&gt;--&gt;
        &lt;!--      &lt;/template&gt;--&gt;
        &lt;v-card tile&gt;
          &lt;v-card-title&gt;
            &lt;span class="text-h5"&gt;{{ (type === 'add' ? '新增' : '编辑') + title }}&lt;/span&gt;
          &lt;/v-card-title&gt;
          &lt;v-card-text&gt;
            &lt;v-container&gt;
              &lt;v-row&gt;
                &lt;v-col :cols="12" :md="item.cols" v-for="(item,i) in formModel" :key="i"&gt;
                  &lt;template v-if="item.formType === 'text'"&gt;
                    &lt;template v-if="item.notNull"&gt;
                      &lt;v-text-field dense ref="inputRef"
                                    :label="item.name" :rules="[() =&gt; !!item.value || item.name+'不能为空']"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' &amp;&amp; item.readonly"
                      &gt;&lt;/v-text-field&gt;
                    &lt;/template&gt;
                    &lt;template v-if="!item.notNull"&gt;
                      &lt;v-text-field dense ref="inputRef"
                                    :label="item.name"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' &amp;&amp; item.readonly"&gt;&lt;/v-text-field&gt;
                    &lt;/template&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'number'"&gt;
                    &lt;template v-if="item.notNull"&gt;
                      &lt;v-text-field dense ref="inputRef" type="number"
                                    :label="item.name" :rules="[() =&gt; !!item.value || item.name+'不能为空']"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' &amp;&amp; item.readonly"
                      &gt;&lt;/v-text-field&gt;
                    &lt;/template&gt;
                    &lt;template v-if="!item.notNull"&gt;
                      &lt;v-text-field dense ref="inputRef" type="number"
                                    :label="item.name"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' &amp;&amp; item.readonly"&gt;&lt;/v-text-field&gt;
                    &lt;/template&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'select'"&gt;
                    &lt;v-select ref="selectRef" :rules="[v =&gt; ((!!v)|| v === 0)||'this is required!']"
                              v-model="item.value"
                              :items="item.option" :value="item.value" item-text="label" item-value="value"
                              :label="item.name"
                              dense
                    &gt;&lt;/v-select&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'autocomplete'"&gt;
                    &lt;v-autocomplete
                      v-model="item.value"
                      :items="item.items"
                      :loading="item.loading"
                      :search-input.sync="item.searchText"
                      dense
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      :label="item.name"
                      :placeholder="'请输入' + item.name"
                      :prepend-icon="item.icon"
                    &gt;
                      &lt;template v-if="item.showField" v-slot:item="{item}"&gt;
                        &lt;v-list-item-content&gt;
                          &lt;v-list-item-title v-html="item.Code"&gt;&lt;/v-list-item-title&gt;
                          &lt;v-list-item-subtitle v-html="item.Description"&gt;&lt;/v-list-item-subtitle&gt;
                        &lt;/v-list-item-content&gt;
                      &lt;/template&gt;
                    &lt;/v-autocomplete&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'checkbox'"&gt;
                    &lt;v-checkbox :readonly="item.readonly"
                                v-model="item.value"
                                :label="item.name"
                    &gt;&lt;/v-checkbox&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'textarea'"&gt;
                    &lt;v-textarea ref="inputRef"
                                v-model="item.value"
                    &gt;
                      &lt;template v-slot:label&gt;
                        &lt;div&gt;
                          {{ item.name }}
                        &lt;/div&gt;
                      &lt;/template&gt;
                    &lt;/v-textarea&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'upload'"&gt;
                    &lt;v-file-input ref="inputRef"
                                  :rules="[
          value =&gt; !value || value.size &lt; 2000000 || 'Avatar size should be less than 2 MB!',
        ]"
                                  accept="image/png, image/jpeg, image/bmp"
                                  :placeholder="item.name"
                                  prepend-icon="mdi-camera" v-model="item.value"
                                  :label="item.name"
                    &gt;&lt;/v-file-input&gt;
                  &lt;/template&gt;
                &lt;/v-col&gt;
              &lt;/v-row&gt;
            &lt;/v-container&gt;
          &lt;/v-card-text&gt;
          &lt;v-card-actions&gt;
            &lt;v-spacer&gt;&lt;/v-spacer&gt;
            &lt;v-btn
              color="primary darken-1"
              text tile
              @click="dialog = false"
            &gt;
              关闭
            &lt;/v-btn&gt;
            &lt;v-slide-x-reverse-transition&gt;
              &lt;v-tooltip
                v-if="formHasErrors"
                left
              &gt;
                &lt;template v-slot:activator="{ on, attrs }"&gt;
                  &lt;v-btn
                    icon
                    class="my-0"
                    v-bind="attrs"
                    @click="reset"
                    v-on="on"
                  &gt;
                    &lt;v-icon&gt;mdi-refresh&lt;/v-icon&gt;
                  &lt;/v-btn&gt;
                &lt;/template&gt;
                &lt;span&gt;Refresh form&lt;/span&gt;
              &lt;/v-tooltip&gt;
            &lt;/v-slide-x-reverse-transition&gt;
            &lt;v-btn tile
                   color="primary darken-1"
                   @click="submit"
            &gt;
              保存
            &lt;/v-btn&gt;
          &lt;/v-card-actions&gt;
        &lt;/v-card&gt;
      &lt;/v-dialog&gt;
    &lt;/v-row&gt;
  &lt;/template&gt;

  &lt;script&gt;
  import request from '../../axios/request'

  export default {
    name: 'CommonForm',
    props: ['type', 'title', 'formModel', 'updateUrl', 'addUrl'],
    data () {
      return {
        visible: false,
        formModelReset: JSON.parse(JSON.stringify(this.formModel)),
        formModelData: this.formModel,
        dialog: false,
        formHasErrors: false,
        isHaveSelect: false,
        lock: false,
        saveLastOption: []
      }
    },
    beforeCreate () {
      this.$nextTick(() =&gt; {
        this.formModelData.forEach((item) =&gt; {
          if (item.formType === 'autocomplete') {
            // eslint-disable-next-line no-prototype-builtins
            if (item.hasOwnProperty('searchText')) {
              item.searchText = ''
            }
          }
        })
      })
    },
    watch: {
      dialog (val) {
        if (!val) {
          this.reset()
        }
      },
      formModelData: {
        handler: function (value, old) {
          console.log('exce w ' + this.lock)

          console.log('exce w ' + this.lock)
          const items = value.filter((item) =&gt; item.formType === 'autocomplete')
          for (let i = 0; i &lt; items.length; i++) {
            if (this.saveLastOption.length === 0) {
              console.log('1')

              this.saveLastOption.push({
                index: i,
                searchText: items[i].searchText
              })
            } else {
              const option = this.saveLastOption.filter((item) =&gt; item.index === i)
              if (option.length &gt; 0) {
                if (option[0].searchText === items[i].searchText) {
                  this.lock = true
                } else {
                  this.lock = false
                  option[0].searchText = items[i].searchText
                }
              }
            }
          }

          if (this.lock === false) {
            for (let i = 0; i &lt; value.length; i++) {
              // eslint-disable-next-line no-prototype-builtins
              if (value[i].formType === 'autocomplete' &amp;&amp; (value[i].searchText !== null) &amp;&amp; value[i].hasOwnProperty('searchText')) {
                this.lock = true
                const params = value[i].requestModel.params
                if (value[i].requestModel.activeParam) {
                  params[value[i].requestModel.activeParam] = value[i].searchText
                }
                value[i].loading = true
                request({
                  url: value[i].requestModel.url,
                  method: value[i].requestModel.methods,
                  params: value[i].requestModel.activeParam ? {} : params
                })
                  .then((resp) =&gt; {
                    this.$set(this.formModelData[i], 'items', resp.data)
                    this.$set(this.formModelData[i], 'loading', false)
                    // check join
                    this.execJoin(value[i], value)
                  }).finally(() =&gt; {
                    this.lock = false
                  })
              }
            }
          }
        },
        deep: true
      }
    },
    methods: {
      execJoin (item, value) {
        // eslint-disable-next-line no-prototype-builtins
        if (item.hasOwnProperty('joint')) {
          if (!Array.isArray(item.joint)) {
            const joinModel = value.filter((j) =&gt; j.code === item.joint)[0]
            this.exec(joinModel, item)
          } else {
            const joinModels = value.filter((j) =&gt; item.joint.indexOf(j.code) &gt; -1)
            for (let i = 0; i &lt; joinModels.length; i++) {
              this.exec(joinModels[i], item)
            }
          }
        }
      },
      exec (joinModel, item) {
        const params = joinModel.requestModel.params
        if (!this.$util.isNullOrUndefined(item.value) &amp;&amp; !this.$util.isNullOrUndefined(joinModel)) {
          params[joinModel.requestModel.activeParam] = item.value
          this.lock = true
          console.log('exec join')

          joinModel.loading = true
          request({
            url: joinModel.requestModel.url,
            method: joinModel.requestModel.methods,
            params: params
          })
            .then((resp) =&gt; {
              this.$set(joinModel, 'items', resp.data)
              this.$set(joinModel, 'loading', false)

              // check join
            }).finally(() =&gt; {
              joinModel.loading = false
            })
        }
      },
      open () {
        this.dialog = true
        this.isHaveSelect = this.formModel.filter((item) =&gt; item.formType === 'select').length &gt; 0
      },
      close () {
        this.dialog = false
      },
      reset () {
        this.$refs.inputRef.forEach((ref) =&gt; {
          ref.reset()
        })
        if (this.isHaveSelect) {
        }
      },
      submit () {
        this.formHasErrors = false
        this.$refs.inputRef.forEach((ref) =&gt; {
          if (!ref.validate()) {
            this.formHasErrors = true
          }
          ref.validate(true)
        })
        if (this.isHaveSelect) {
          this.$refs.selectRef.forEach((ref) =&gt; {
            if (!ref.validate()) {
              this.formHasErrors = true
            }
            ref.validate(true)
          })
        }
        const formBody = new FormData()
        this.formModelData.forEach((item) =&gt; {
          formBody.append([item.code], item.value)
          console.log(item.value)
        })
        console.log(formBody)
        if (!this.formHasErrors) {
          request({
            url: this.type === 'add' ? this.addUrl : this.updateUrl,
            method: 'post',
            data: formBody
          })
            .then((resp) =&gt; {
              console.log(resp)
              this.close()
              this.$emit('done')
            })
            .catch((resp) =&gt; {

            })
        }
      }
    }
  }
  &lt;/script&gt;

  &lt;style scoped&gt;

  &lt;/style&gt;
  </code></pre>
  <p>使用:</p>
  <pre><code class="lang-vue">&lt;CommonForm :type="type" add-url="/Region/Create" update-url="/Region/Update" :form-model="formModel"
              v-on:done="search"
              title="区域"
              ref="formDialog"&gt;&lt;/CommonForm&gt;
  </code></pre>
  <p>查询模板与表单模板类似：</p>
  <p>Query Model：</p>
  <pre><code class="lang-json">queryModel: [
    {
      name: '区域代码',
      code: 'code',
      value: '',
      formType: 'text'
    },
    {
      name: '工厂',
      code: 'plant',
      value: '',
      formType: 'autocomplete',
      loading: false,
      searchText: null,
      selectText: 'Name',
      selectValue: 'Code',
      icon: 'mdi-factory',
      requestModel: {
        url: '/Plant/Search',
        params: {
          code: '',
          name: ''
        },
        activeParam: 'name',
        methods: 'post'
      }
    },
    {
      name: '区域名称',
      code: 'name',
      value: '',
      formType: 'text'
    }
  ],
  </code></pre>
  <p>CommonQueryCard.vue 源码：</p>
  <pre><code class="lang-vue">&lt;!-- shit mountain. Don't touch.  Just Use--&gt;
  &lt;template&gt;
    &lt;v-card tile&gt;
      &lt;v-card-actions @click="show = !show"&gt;
        &lt;v-btn tile text class="text-h6"&gt;
          查询条件
        &lt;/v-btn&gt;

        &lt;v-spacer&gt;&lt;/v-spacer&gt;

        &lt;v-btn icon&gt;
          &lt;v-icon&gt;{{ show ? 'mdi-chevron-up' : 'mdi-chevron-down' }}&lt;/v-icon&gt;
        &lt;/v-btn&gt;
      &lt;/v-card-actions&gt;
      &lt;v-expand-transition&gt;
        &lt;div v-show="show"&gt;
          &lt;v-divider&gt;&lt;/v-divider&gt;
          &lt;v-card-text&gt;
            &lt;v-form ref="form"&gt;
              &lt;v-row&gt;
                &lt;v-col v-for="(item,i) in this.queryModelData" :cols="col" :key="i"&gt;
                  &lt;template v-if="item.formType === 'text'"&gt;
                    &lt;v-text-field
                      dense v-model="item.value"
                      :placeholder="'请输入' + item.name"
                      :label="item.name"
                    &gt;&lt;/v-text-field&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'select'"&gt;
                    &lt;v-select
                      :items="item.option"
                      dense
                      :label="item.name"
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      v-model="item.value"
                      outlined
                      :placeholder="'请输入' + item.name"
                    &gt;&lt;/v-select&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'autocomplete'"&gt;
                    &lt;v-autocomplete
                      v-model="item.value"
                      :items="item.items"
                      :loading="item.loading" clearable
                      :search-input.sync="item.searchText"
                      dense
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      :label="item.name"
                      :placeholder="'请输入' + item.name"
                      :prepend-icon="item.icon"
                    &gt;&lt;/v-autocomplete&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'checkbox'"&gt;
                    &lt;v-checkbox
                      v-model="item.value" dense
                      :label="item.name"
                    &gt;&lt;/v-checkbox&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'datetime-range'"&gt;
                    &lt;v-menu
                        v-model="menu2"
                        :close-on-content-click="false"
                        :nudge-right="40"
                        transition="scale-transition"
                        offset-y
                        min-width="auto"
                    &gt;
                      &lt;template v-slot:activator="{ on, attrs }"&gt;
                        &lt;v-text-field dense
                            v-model="item.value"
                                      :label="item.name"
                            prepend-icon="mdi-calendar"
                            readonly
                            v-bind="attrs"
                            v-on="on"
                        &gt;&lt;/v-text-field&gt;
                      &lt;/template&gt;
                      &lt;v-date-picker
                          v-model="item.value" range
                          @input="menu2 = false"
                      &gt;&lt;/v-date-picker&gt;
                    &lt;/v-menu&gt;
                  &lt;/template&gt;
                  &lt;template v-if="item.formType === 'datetime'"&gt;
                    &lt;v-menu
                        v-model="menu2"
                        :close-on-content-click="false"
                        :nudge-right="40"
                        transition="scale-transition"
                        offset-y
                        min-width="auto"
                    &gt;
                      &lt;template v-slot:activator="{ on, attrs }"&gt;
                        &lt;v-text-field dense
                                      v-model="item.value"
                                      :label="item.name"
                                      prepend-icon="mdi-calendar"
                                      readonly
                                      v-bind="attrs"
                                      v-on="on"
                        &gt;&lt;/v-text-field&gt;
                      &lt;/template&gt;
                      &lt;v-date-picker
                          v-model="item.value"
                          @input="menu2 = false"
                      &gt;&lt;/v-date-picker&gt;
                    &lt;/v-menu&gt;
                  &lt;/template&gt;
                &lt;/v-col&gt;
              &lt;/v-row&gt;
            &lt;/v-form&gt;
          &lt;/v-card-text&gt;
          &lt;v-divider&gt;&lt;/v-divider&gt;
          &lt;v-card-actions&gt;
            &lt;v-spacer /&gt;
            &lt;v-btn tile color="primary" @click="search"
            &gt;查询
            &lt;/v-btn&gt;
          &lt;/v-card-actions&gt;
        &lt;/div&gt;
      &lt;/v-expand-transition&gt;
    &lt;/v-card&gt;
  &lt;/template&gt;

  &lt;script&gt;
  import request from '../../axios/request'
  export default {
    name: 'CommonQueryCard',
    props: ['col', 'queryModel'],
    components:
    {
    },
    data () {
      return {
        show: false,
        queryModelReset: JSON.parse(JSON.stringify(this.queryModel)),
        queryModelData: this.queryModel,
        lock: false,
        saveLastOption: []
      }
    },
    beforeCreate () {
      this.$nextTick(() =&gt; {
        this.queryModelData.forEach((item) =&gt; {
          if (item.formType === 'autocomplete') {
            // eslint-disable-next-line no-prototype-builtins
            if (item.hasOwnProperty('searchText')) {
              item.searchText = ''
            }
          }
        })
      })
    },
    methods: {
      search () {
        const params = {}
        for (let i = 0; i &lt; this.queryModelData.length; i++) {
          params[this.queryModelData[i].code] = this.queryModelData[i].value
        }
        this.$emit('searchDone', params)
      }
    },
    watch: {
      queryModelData: {
        handler: function (value, old) {
          console.log('exce w ' + this.lock)

          console.log('exce w ' + this.lock)
          const items = value.filter((item) =&gt; item.formType === 'autocomplete')
          for (let i = 0; i &lt; items.length; i++) {
            if (this.saveLastOption.length === 0) {
              console.log('1')

              this.saveLastOption.push({
                index: i,
                searchText: items[i].searchText
              })
            } else {
              const option = this.saveLastOption.filter((item) =&gt; item.index === i)
              if (option.length &gt; 0) {
                if (option[0].searchText === items[i].searchText) {
                  this.lock = true
                } else {
                  this.lock = false
                  option[0].searchText = items[i].searchText
                }
              }
            }
          }

          if (this.lock === false) {
            value.forEach((item, i) =&gt; {
              // eslint-disable-next-line no-prototype-builtins
              if (item.formType === 'autocomplete' &amp;&amp; (item.searchText !== null) &amp;&amp; item.hasOwnProperty('searchText')) {
                this.lock = true
                const params = item.requestModel.params
                if (item.requestModel.activeParam) {
                  params[item.requestModel.activeParam] = item.searchText
                }
                item.loading = true
                request({
                  url: item.requestModel.url,
                  method: item.requestModel.methods,
                  params: item.requestModel.activeParam ? {} : params
                })
                  .then((resp) =&gt; {
                    this.$set(this.queryModelData[i], 'items', resp.data)
                    this.$set(this.queryModelData[i], 'loading', false)
                    // check join
                    const joinModel = value.filter((j) =&gt; j.code === item.joint)[0]

                    if (joinModel !== undefined) {
                      const params = joinModel.requestModel.params

                      if (!this.$util.isNullOrUndefined(item.value) &amp;&amp; !this.$util.isNullOrUndefined(joinModel)) {
                        params[joinModel.requestModel.activeParam] = item.value
                        this.lock = true
                        console.log('exec join')

                        joinModel.loading = true
                        request({
                          url: joinModel.requestModel.url,
                          method: joinModel.requestModel.methods,
                          params: params
                        })
                          .then((resp) =&gt; {
                            this.$set(joinModel, 'items', resp.data)
                            this.$set(joinModel, 'loading', false)

                            // check join
                          }).finally(() =&gt; {
                            joinModel.loading = false
                          })
                      }
                    }
                  }).finally(() =&gt; {
                    this.lock = false
                  })
              }
            })
          }
        },
        deep: true
      }
    }
  }
  &lt;/script&gt;

  &lt;style scoped&gt;

  &lt;/style&gt;
  </code></pre>
  <p>使用：</p>
  <pre><code class="lang-vue">&lt;CommonQueryCard :query-model="queryModel" v-on:searchDone="search" :col="4"&gt;&lt;/CommonQueryCard&gt;
  </code></pre>
  <p>生成界面如下：</p>
  <p><img src="/upload/files/png/739e655ccc72d0a479b127f95d9f5997.png" alt="">

markdown_text:
  #### 前言

  在使用Vuetify做前台的时候，大量CRUD的功能十分繁琐，而且会让VUE代码变得混乱难以维护，那么对CRUD的组件化封装必不可少。

  首先需要一个JSON数据结构来构建界面，通过参数化配置，可以灵活生成表单。

  我封装的表单支持，text，select，AutoComplate(可以请求，异步查询)，checkbox，number

  #### Example

  ```json
    formModel: [
      {
        code: 'Code',
        name: '区域代码',
        formType: 'text', //表单类型  text，select，AutoComplate，checkbox，number
        value: '',
        notNull: true, //是否为空
        col: 6, //布局
        readonly: false //是否只读
      },
      {
        code: 'Name',
        name: '区域名称',
        formType: 'text',
        value: '',
        notNull: true,
        col: 6
      },
      {
        name: '工厂',
        code: 'Plant',
        value: '',
        formType: 'autocomplete',
        loading: false, //加载
        searchText: null, //查询文本
        selectText: 'Name',//参数对应
        selectValue: 'Code',//参数对应
        icon: 'mdi-factory',//查询图标
        requestModel: { //请求参数构建
          url: '/Plant/Search', 
          params: {
            code: '',
            name: ''
          },
          activeParam: 'name', //在查询时，只有一个查询条件会变化，也就是autocomplates组件在查询时会将searchText赋值给params的name，code不会变
          methods: 'post'
        },
        notNull: true
      },
      {
        code: 'Version',
        name: '版本',
        formType: 'number',
        value: 1,
        notNull: true,
        col: 6
      },
      {
        code: 'IsActive',
        name: '生效',
        formType: 'check',
        value: true,
        notNull: true,
        col: 6
      }
    ]
  ```

  会生成以下表单：

  ![](/upload/files/png/6a6bf79d22dd5573cf47a740ac158851.png)



  修改可以通过从表格获取item数据，在将值赋给formModel，同时在内部完成接口请求操作。

  CommonForm.vue 源码

  ```vue
  <!-- shit mountain. Don't touch.  Just Use-->
  <template>
    <v-row justify="center">
      <v-dialog
        v-model="dialog"
        persistent
        max-width="600px"
      >
        <!--      <template v-slot:activator="{ on, attrs }">-->
        <!--        <v-btn-->
        <!--          color="primary"-->
        <!--          dark-->
        <!--          v-bind="attrs"-->
        <!--          v-on="on"-->
        <!--        >-->
        <!--          Open Dialog-->
        <!--        </v-btn>-->
        <!--      </template>-->
        <v-card tile>
          <v-card-title>
            <span class="text-h5">{{ (type === 'add' ? '新增' : '编辑') + title }}</span>
          </v-card-title>
          <v-card-text>
            <v-container>
              <v-row>
                <v-col :cols="12" :md="item.cols" v-for="(item,i) in formModel" :key="i">
                  <template v-if="item.formType === 'text'">
                    <template v-if="item.notNull">
                      <v-text-field dense ref="inputRef"
                                    :label="item.name" :rules="[() => !!item.value || item.name+'不能为空']"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' && item.readonly"
                      ></v-text-field>
                    </template>
                    <template v-if="!item.notNull">
                      <v-text-field dense ref="inputRef"
                                    :label="item.name"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' && item.readonly"></v-text-field>
                    </template>
                  </template>
                  <template v-if="item.formType === 'number'">
                    <template v-if="item.notNull">
                      <v-text-field dense ref="inputRef" type="number"
                                    :label="item.name" :rules="[() => !!item.value || item.name+'不能为空']"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' && item.readonly"
                      ></v-text-field>
                    </template>
                    <template v-if="!item.notNull">
                      <v-text-field dense ref="inputRef" type="number"
                                    :label="item.name"
                                    v-model="item.value"
                                    :placeholder="item.name" :disabled="type!=='add' && item.readonly"></v-text-field>
                    </template>
                  </template>
                  <template v-if="item.formType === 'select'">
                    <v-select ref="selectRef" :rules="[v => ((!!v)|| v === 0)||'this is required!']"
                              v-model="item.value"
                              :items="item.option" :value="item.value" item-text="label" item-value="value"
                              :label="item.name"
                              dense
                    ></v-select>
                  </template>
                  <template v-if="item.formType === 'autocomplete'">
                    <v-autocomplete
                      v-model="item.value"
                      :items="item.items"
                      :loading="item.loading"
                      :search-input.sync="item.searchText"
                      dense
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      :label="item.name"
                      :placeholder="'请输入' + item.name"
                      :prepend-icon="item.icon"
                    >
                      <template v-if="item.showField" v-slot:item="{item}">
                        <v-list-item-content>
                          <v-list-item-title v-html="item.Code"></v-list-item-title>
                          <v-list-item-subtitle v-html="item.Description"></v-list-item-subtitle>
                        </v-list-item-content>
                      </template>
                    </v-autocomplete>
                  </template>
                  <template v-if="item.formType === 'checkbox'">
                    <v-checkbox :readonly="item.readonly"
                                v-model="item.value"
                                :label="item.name"
                    ></v-checkbox>
                  </template>
                  <template v-if="item.formType === 'textarea'">
                    <v-textarea ref="inputRef"
                                v-model="item.value"
                    >
                      <template v-slot:label>
                        <div>
                          {{ item.name }}
                        </div>
                      </template>
                    </v-textarea>
                  </template>
                  <template v-if="item.formType === 'upload'">
                    <v-file-input ref="inputRef"
                                  :rules="[
          value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!',
        ]"
                                  accept="image/png, image/jpeg, image/bmp"
                                  :placeholder="item.name"
                                  prepend-icon="mdi-camera" v-model="item.value"
                                  :label="item.name"
                    ></v-file-input>
                  </template>
                </v-col>
              </v-row>
            </v-container>
          </v-card-text>
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn
              color="primary darken-1"
              text tile
              @click="dialog = false"
            >
              关闭
            </v-btn>
            <v-slide-x-reverse-transition>
              <v-tooltip
                v-if="formHasErrors"
                left
              >
                <template v-slot:activator="{ on, attrs }">
                  <v-btn
                    icon
                    class="my-0"
                    v-bind="attrs"
                    @click="reset"
                    v-on="on"
                  >
                    <v-icon>mdi-refresh</v-icon>
                  </v-btn>
                </template>
                <span>Refresh form</span>
              </v-tooltip>
            </v-slide-x-reverse-transition>
            <v-btn tile
                   color="primary darken-1"
                   @click="submit"
            >
              保存
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>
    </v-row>
  </template>

  <script>
  import request from '../../axios/request'

  export default {
    name: 'CommonForm',
    props: ['type', 'title', 'formModel', 'updateUrl', 'addUrl'],
    data () {
      return {
        visible: false,
        formModelReset: JSON.parse(JSON.stringify(this.formModel)),
        formModelData: this.formModel,
        dialog: false,
        formHasErrors: false,
        isHaveSelect: false,
        lock: false,
        saveLastOption: []
      }
    },
    beforeCreate () {
      this.$nextTick(() => {
        this.formModelData.forEach((item) => {
          if (item.formType === 'autocomplete') {
            // eslint-disable-next-line no-prototype-builtins
            if (item.hasOwnProperty('searchText')) {
              item.searchText = ''
            }
          }
        })
      })
    },
    watch: {
      dialog (val) {
        if (!val) {
          this.reset()
        }
      },
      formModelData: {
        handler: function (value, old) {
          console.log('exce w ' + this.lock)

          console.log('exce w ' + this.lock)
          const items = value.filter((item) => item.formType === 'autocomplete')
          for (let i = 0; i < items.length; i++) {
            if (this.saveLastOption.length === 0) {
              console.log('1')

              this.saveLastOption.push({
                index: i,
                searchText: items[i].searchText
              })
            } else {
              const option = this.saveLastOption.filter((item) => item.index === i)
              if (option.length > 0) {
                if (option[0].searchText === items[i].searchText) {
                  this.lock = true
                } else {
                  this.lock = false
                  option[0].searchText = items[i].searchText
                }
              }
            }
          }

          if (this.lock === false) {
            for (let i = 0; i < value.length; i++) {
              // eslint-disable-next-line no-prototype-builtins
              if (value[i].formType === 'autocomplete' && (value[i].searchText !== null) && value[i].hasOwnProperty('searchText')) {
                this.lock = true
                const params = value[i].requestModel.params
                if (value[i].requestModel.activeParam) {
                  params[value[i].requestModel.activeParam] = value[i].searchText
                }
                value[i].loading = true
                request({
                  url: value[i].requestModel.url,
                  method: value[i].requestModel.methods,
                  params: value[i].requestModel.activeParam ? {} : params
                })
                  .then((resp) => {
                    this.$set(this.formModelData[i], 'items', resp.data)
                    this.$set(this.formModelData[i], 'loading', false)
                    // check join
                    this.execJoin(value[i], value)
                  }).finally(() => {
                    this.lock = false
                  })
              }
            }
          }
        },
        deep: true
      }
    },
    methods: {
      execJoin (item, value) {
        // eslint-disable-next-line no-prototype-builtins
        if (item.hasOwnProperty('joint')) {
          if (!Array.isArray(item.joint)) {
            const joinModel = value.filter((j) => j.code === item.joint)[0]
            this.exec(joinModel, item)
          } else {
            const joinModels = value.filter((j) => item.joint.indexOf(j.code) > -1)
            for (let i = 0; i < joinModels.length; i++) {
              this.exec(joinModels[i], item)
            }
          }
        }
      },
      exec (joinModel, item) {
        const params = joinModel.requestModel.params
        if (!this.$util.isNullOrUndefined(item.value) && !this.$util.isNullOrUndefined(joinModel)) {
          params[joinModel.requestModel.activeParam] = item.value
          this.lock = true
          console.log('exec join')

          joinModel.loading = true
          request({
            url: joinModel.requestModel.url,
            method: joinModel.requestModel.methods,
            params: params
          })
            .then((resp) => {
              this.$set(joinModel, 'items', resp.data)
              this.$set(joinModel, 'loading', false)

              // check join
            }).finally(() => {
              joinModel.loading = false
            })
        }
      },
      open () {
        this.dialog = true
        this.isHaveSelect = this.formModel.filter((item) => item.formType === 'select').length > 0
      },
      close () {
        this.dialog = false
      },
      reset () {
        this.$refs.inputRef.forEach((ref) => {
          ref.reset()
        })
        if (this.isHaveSelect) {
        }
      },
      submit () {
        this.formHasErrors = false
        this.$refs.inputRef.forEach((ref) => {
          if (!ref.validate()) {
            this.formHasErrors = true
          }
          ref.validate(true)
        })
        if (this.isHaveSelect) {
          this.$refs.selectRef.forEach((ref) => {
            if (!ref.validate()) {
              this.formHasErrors = true
            }
            ref.validate(true)
          })
        }
        const formBody = new FormData()
        this.formModelData.forEach((item) => {
          formBody.append([item.code], item.value)
          console.log(item.value)
        })
        console.log(formBody)
        if (!this.formHasErrors) {
          request({
            url: this.type === 'add' ? this.addUrl : this.updateUrl,
            method: 'post',
            data: formBody
          })
            .then((resp) => {
              console.log(resp)
              this.close()
              this.$emit('done')
            })
            .catch((resp) => {

            })
        }
      }
    }
  }
  </script>

  <style scoped>

  </style>

  ```

  使用:

  ```vue
  <CommonForm :type="type" add-url="/Region/Create" update-url="/Region/Update" :form-model="formModel"
              v-on:done="search"
              title="区域"
              ref="formDialog"></CommonForm>
  ```



  查询模板与表单模板类似：

  Query Model：

  ```json
  queryModel: [
    {
      name: '区域代码',
      code: 'code',
      value: '',
      formType: 'text'
    },
    {
      name: '工厂',
      code: 'plant',
      value: '',
      formType: 'autocomplete',
      loading: false,
      searchText: null,
      selectText: 'Name',
      selectValue: 'Code',
      icon: 'mdi-factory',
      requestModel: {
        url: '/Plant/Search',
        params: {
          code: '',
          name: ''
        },
        activeParam: 'name',
        methods: 'post'
      }
    },
    {
      name: '区域名称',
      code: 'name',
      value: '',
      formType: 'text'
    }
  ],
  ```

  CommonQueryCard.vue 源码：

  ```vue
  <!-- shit mountain. Don't touch.  Just Use-->
  <template>
    <v-card tile>
      <v-card-actions @click="show = !show">
        <v-btn tile text class="text-h6">
          查询条件
        </v-btn>

        <v-spacer></v-spacer>

        <v-btn icon>
          <v-icon>{{ show ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
        </v-btn>
      </v-card-actions>
      <v-expand-transition>
        <div v-show="show">
          <v-divider></v-divider>
          <v-card-text>
            <v-form ref="form">
              <v-row>
                <v-col v-for="(item,i) in this.queryModelData" :cols="col" :key="i">
                  <template v-if="item.formType === 'text'">
                    <v-text-field
                      dense v-model="item.value"
                      :placeholder="'请输入' + item.name"
                      :label="item.name"
                    ></v-text-field>
                  </template>
                  <template v-if="item.formType === 'select'">
                    <v-select
                      :items="item.option"
                      dense
                      :label="item.name"
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      v-model="item.value"
                      outlined
                      :placeholder="'请输入' + item.name"
                    ></v-select>
                  </template>
                  <template v-if="item.formType === 'autocomplete'">
                    <v-autocomplete
                      v-model="item.value"
                      :items="item.items"
                      :loading="item.loading" clearable
                      :search-input.sync="item.searchText"
                      dense
                      :item-text="item.selectText"
                      :item-value="item.selectValue"
                      :label="item.name"
                      :placeholder="'请输入' + item.name"
                      :prepend-icon="item.icon"
                    ></v-autocomplete>
                  </template>
                  <template v-if="item.formType === 'checkbox'">
                    <v-checkbox
                      v-model="item.value" dense
                      :label="item.name"
                    ></v-checkbox>
                  </template>
                  <template v-if="item.formType === 'datetime-range'">
                    <v-menu
                        v-model="menu2"
                        :close-on-content-click="false"
                        :nudge-right="40"
                        transition="scale-transition"
                        offset-y
                        min-width="auto"
                    >
                      <template v-slot:activator="{ on, attrs }">
                        <v-text-field dense
                            v-model="item.value"
                                      :label="item.name"
                            prepend-icon="mdi-calendar"
                            readonly
                            v-bind="attrs"
                            v-on="on"
                        ></v-text-field>
                      </template>
                      <v-date-picker
                          v-model="item.value" range
                          @input="menu2 = false"
                      ></v-date-picker>
                    </v-menu>
                  </template>
                  <template v-if="item.formType === 'datetime'">
                    <v-menu
                        v-model="menu2"
                        :close-on-content-click="false"
                        :nudge-right="40"
                        transition="scale-transition"
                        offset-y
                        min-width="auto"
                    >
                      <template v-slot:activator="{ on, attrs }">
                        <v-text-field dense
                                      v-model="item.value"
                                      :label="item.name"
                                      prepend-icon="mdi-calendar"
                                      readonly
                                      v-bind="attrs"
                                      v-on="on"
                        ></v-text-field>
                      </template>
                      <v-date-picker
                          v-model="item.value"
                          @input="menu2 = false"
                      ></v-date-picker>
                    </v-menu>
                  </template>
                </v-col>
              </v-row>
            </v-form>
          </v-card-text>
          <v-divider></v-divider>
          <v-card-actions>
            <v-spacer />
            <v-btn tile color="primary" @click="search"
            >查询
            </v-btn>
          </v-card-actions>
        </div>
      </v-expand-transition>
    </v-card>
  </template>

  <script>
  import request from '../../axios/request'
  export default {
    name: 'CommonQueryCard',
    props: ['col', 'queryModel'],
    components:
    {
    },
    data () {
      return {
        show: false,
        queryModelReset: JSON.parse(JSON.stringify(this.queryModel)),
        queryModelData: this.queryModel,
        lock: false,
        saveLastOption: []
      }
    },
    beforeCreate () {
      this.$nextTick(() => {
        this.queryModelData.forEach((item) => {
          if (item.formType === 'autocomplete') {
            // eslint-disable-next-line no-prototype-builtins
            if (item.hasOwnProperty('searchText')) {
              item.searchText = ''
            }
          }
        })
      })
    },
    methods: {
      search () {
        const params = {}
        for (let i = 0; i < this.queryModelData.length; i++) {
          params[this.queryModelData[i].code] = this.queryModelData[i].value
        }
        this.$emit('searchDone', params)
      }
    },
    watch: {
      queryModelData: {
        handler: function (value, old) {
          console.log('exce w ' + this.lock)

          console.log('exce w ' + this.lock)
          const items = value.filter((item) => item.formType === 'autocomplete')
          for (let i = 0; i < items.length; i++) {
            if (this.saveLastOption.length === 0) {
              console.log('1')

              this.saveLastOption.push({
                index: i,
                searchText: items[i].searchText
              })
            } else {
              const option = this.saveLastOption.filter((item) => item.index === i)
              if (option.length > 0) {
                if (option[0].searchText === items[i].searchText) {
                  this.lock = true
                } else {
                  this.lock = false
                  option[0].searchText = items[i].searchText
                }
              }
            }
          }

          if (this.lock === false) {
            value.forEach((item, i) => {
              // eslint-disable-next-line no-prototype-builtins
              if (item.formType === 'autocomplete' && (item.searchText !== null) && item.hasOwnProperty('searchText')) {
                this.lock = true
                const params = item.requestModel.params
                if (item.requestModel.activeParam) {
                  params[item.requestModel.activeParam] = item.searchText
                }
                item.loading = true
                request({
                  url: item.requestModel.url,
                  method: item.requestModel.methods,
                  params: item.requestModel.activeParam ? {} : params
                })
                  .then((resp) => {
                    this.$set(this.queryModelData[i], 'items', resp.data)
                    this.$set(this.queryModelData[i], 'loading', false)
                    // check join
                    const joinModel = value.filter((j) => j.code === item.joint)[0]

                    if (joinModel !== undefined) {
                      const params = joinModel.requestModel.params

                      if (!this.$util.isNullOrUndefined(item.value) && !this.$util.isNullOrUndefined(joinModel)) {
                        params[joinModel.requestModel.activeParam] = item.value
                        this.lock = true
                        console.log('exec join')

                        joinModel.loading = true
                        request({
                          url: joinModel.requestModel.url,
                          method: joinModel.requestModel.methods,
                          params: params
                        })
                          .then((resp) => {
                            this.$set(joinModel, 'items', resp.data)
                            this.$set(joinModel, 'loading', false)

                            // check join
                          }).finally(() => {
                            joinModel.loading = false
                          })
                      }
                    }
                  }).finally(() => {
                    this.lock = false
                  })
              }
            })
          }
        },
        deep: true
      }
    }
  }
  </script>

  <style scoped>

  </style>

  ```

  使用：

  ```vue
  <CommonQueryCard :query-model="queryModel" v-on:searchDone="search" :col="4"></CommonQueryCard>
  ```



  生成界面如下：

  ![](/upload/files/png/739e655ccc72d0a479b127f95d9f5997.png)


title: 西门子PLC ModBus TCP 协议读取
intro: 西门子PLC ModBus TCP 协议读取
created_time: 2022-01-07 05:11:35.973687
html_text:
  <h2 id="h2-requirement"><a name="requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>requirement</h2><p>Modbus Slave : <a href="http://s3.z100.vip:28592/modbus/util/3H3modbusslavepjb.rar">http://s3.z100.vip:28592/modbus/util/3H3modbusslavepjb.rar</a></p>
  <p>hslcommunication : <a href="http://www.hslcommunication.cn/">http://www.hslcommunication.cn/</a></p>
  <p>参考：<a href="http://api.hslcommunication.cn/html/8b0255a2-7125-a4ed-7a9c-14314e33603a.htm">http://api.hslcommunication.cn/html/8b0255a2-7125-a4ed-7a9c-14314e33603a.htm</a></p>
  <h2 id="h2--"><a name="使用：" class="reference-link"></a><span class="header-link octicon octicon-link"></span>使用：</h2><p>打开Modbus Slave，根据要求将SID设置为255。</p>
  <p><img src="/upload/files/png/3ccc3a0fb2bc46324360cc9d13124918.png" alt="">
  <p>写了一个winform程序测试。</p>
  <p><img src="/upload/files/png/28dd580f37ef49148960839bf66d2292.png" alt="">
  <p><img src="/upload/files/png/2e5948cda18baa2238c4de673348a231.png" alt="">
  <p>程序下载地址：<a href="http://s3.z100.vip:28592/modbus/app/SIEMENS-PLC-EXAMPLE.zip">http://s3.z100.vip:28592/modbus/app/SIEMENS-PLC-EXAMPLE.zip</a></p>

markdown_text:
  ## requirement

  Modbus Slave : http://s3.z100.vip:28592/modbus/util/3H3modbusslavepjb.rar

  hslcommunication : http://www.hslcommunication.cn/

  参考：http://api.hslcommunication.cn/html/8b0255a2-7125-a4ed-7a9c-14314e33603a.htm

  ## 使用：

  打开Modbus Slave，根据要求将SID设置为255。

  ![](/upload/files/png/3ccc3a0fb2bc46324360cc9d13124918.png)

  写了一个winform程序测试。

  ![](/upload/files/png/28dd580f37ef49148960839bf66d2292.png)

  ![](/upload/files/png/2e5948cda18baa2238c4de673348a231.png)


  程序下载地址：http://s3.z100.vip:28592/modbus/app/SIEMENS-PLC-EXAMPLE.zip


title: Debian 树莓派发布.NET 6 Web 项目
intro: Debian 树莓派发布.NET 6 Web 项目 ##
created_time: 2022-02-04 11:28:17.712818
html_text:
  <h2 id="h2--net-6-sdk-"><a name=".NET 6 SDK安装" class="reference-link"></a><span class="header-link octicon octicon-link"></span>.NET 6 SDK安装</h2><p>下载</p>
  <pre><code class="lang-bash">wget https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-6.0.101-linux-arm32-binaries
  </code></pre>
  <p>环境配置</p>
  <pre><code class="lang-bash">mkdir -p $HOME/dotnet &amp;&amp; tar zxf dotnet-sdk-6.0.101-linux-arm.tar.gz -C $HOME/dotnet
  export DOTNET_ROOT=$HOME/dotnet
  export PATH=$PATH:$HOME/dotnet
  </code></pre>
  <p>执行完成后，发布.NET项目。</p>
  <p><img src="/upload/files/png/fed6d8aca716affb76ee2f0f82e07d8b.png" alt="">
  <p>将Release下的linux-arm文件夹打包上传至Linux解压。</p>
  <h2 id="h2-u8FD0u884C"><a name="运行" class="reference-link"></a><span class="header-link octicon octicon-link"></span>运行</h2><pre><code class="lang-bash">root@raspberrypi:/opt/wistron-run# dotnet SD.Wistron.WebApi.dll
  </code></pre>

markdown_text:
  ## .NET 6 SDK安装

  下载

  ```bash
  wget https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-6.0.101-linux-arm32-binaries
  ```

  环境配置

  ```bash
  mkdir -p $HOME/dotnet && tar zxf dotnet-sdk-6.0.101-linux-arm.tar.gz -C $HOME/dotnet
  export DOTNET_ROOT=$HOME/dotnet
  export PATH=$PATH:$HOME/dotnet
  ```

  执行完成后，发布.NET项目。

  ![](/upload/files/png/fed6d8aca716affb76ee2f0f82e07d8b.png)


  将Release下的linux-arm文件夹打包上传至Linux解压。



  ## 运行

  ```bash
  root@raspberrypi:/opt/wistron-run# dotnet SD.Wistron.WebApi.dll
  ```


title: Zebra PDA 开发
intro: Zebra PDA TC20 广播扫码 内嵌H5 WebView解决方案
created_time: 2022-02-07 15:13:52.189636
html_text:
  <h1 id="h1-zebra-pda-tc20-h5-webview-"><a name="Zebra PDA TC20 广播扫码 内嵌H5 WebView解决方案" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Zebra PDA TC20 广播扫码 内嵌H5 WebView解决方案</h1><h2 id="h2-webview"><a name="Webview" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Webview</h2><p>用Vue创建了一个Vuetify的项目，跑起来用Android Webview本地加载的时候报错：</p>
  <pre><code class="lang-tex">We‘re sorry but XXX doesn‘t work properly without JavaScript enabled. Please enable it to continue.
  </code></pre>
  <p>这一看就是Webview没启用JavaScript？网上查了一下Webview的API，OK了。</p>
  <pre><code class="lang-java">  @SuppressLint("SetJavaScriptEnabled")
      private void initWebView() {
          WebView webView = findViewById(R.id.pda_view);
          webView.loadUrl("http://172.20.10.3:8080/#/");
          webView.setWebChromeClient(new WebChromeClient() {
              @Override
              public void onProgressChanged(WebView view, int newProgress) {
                  super.onProgressChanged(view, newProgress);
                  if (newProgress &gt;= 100) {
                      Log.d(TAG, "onProgressChanged: OK");
                  }
              }
          });
          WebSettings webSettings = webView.getSettings();
          webSettings.setJavaScriptEnabled(true);
          String cacheDirPath = SdApplication.getInstance().getFilesDir().getAbsolutePath() + "cache/";
          webSettings.setAppCachePath(cacheDirPath);
          // 1. 设置缓存路径

          webSettings.setAppCacheMaxSize(20 * 1024 * 1024);
          // 2. 设置缓存大小

          webSettings.setAppCacheEnabled(true);
      }
  </code></pre>
  <h2 id="h2-u5E7Fu64ADu626Bu7801"><a name="广播扫码" class="reference-link"></a><span class="header-link octicon octicon-link"></span>广播扫码</h2><p>这个是需要借助厂商提供内置的APP来配合完成，我去斑马官网找了相关文档。</p>
  <p><a href="https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/">https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/</a></p>
  <p>这里有很详细的说明。</p>
  <p>需要在DataWedge中启用intent广播，然后自定义Action，在Android注册广播时带上设置的Action，就可以收到了。</p>
  <pre><code class="lang-java">package com.wistron_pda.broadcast;

  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.os.Bundle;
  import android.widget.EditText;

  import com.wistron_pda.R;
  import com.wistron_pda.core.SdApplication;

  public class ScanCodeBroadcastReceiver extends BroadcastReceiver {

      private OnReceiveCode onReceive;


      public interface OnReceiveCode {
          void ReceiveBarcodeCallback(String Code);
      }

      public void setOnReceive(OnReceiveCode onReceive) {
          this.onReceive = onReceive;
      }

      @Override
      public void onReceive(Context context, Intent intent) {

          String decodedSource = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_source));
          String decodedData = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_data));
          String decodedLabelType = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_label_type));

          onReceive.ReceiveBarcodeCallback(decodedData);
      }
  }
  </code></pre>
  <p>一个简单的扫码回调接口。</p>
  <p>Activity实现广播类中提供的接口，实现。</p>
  <pre><code class="lang-java">    private void initBroadcastReceiver() {

          setOnReceive = new ScanCodeBroadcastReceiver();
  //        IntentFilter intentFilter = new IntentFilter();
  //        intentFilter.addAction("com.android.scanservice.scancontext");
          IntentFilter filter = new IntentFilter();
          filter.addCategory(Intent.CATEGORY_DEFAULT);
          filter.addAction(getResources().getString(R.string.activity_intent_filter_action));
          registerReceiver(setOnReceive, filter);
          setOnReceive.setOnReceive(this);
      }


      @Override
      public void ReceiveBarcodeCallback(String Code) {
          Log.d(TAG, "条码：: " + Code);
      }
  </code></pre>
  <p>初始化时将Action传进去，回调再配置一下，就可以了。</p>
  <p>activity_intent_filter_action 是 DataWedge 配置的Action。</p>
  <p>简单。</p>
  <hr>
  <p><a href="https://www.cnblogs.com/yangchaojie/p/9660819.html">https://www.cnblogs.com/yangchaojie/p/9660819.html</a></p>
  <p><a href="https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/">https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/</a></p>

markdown_text:
  # Zebra PDA TC20 广播扫码 内嵌H5 WebView解决方案

  ## Webview

  用Vue创建了一个Vuetify的项目，跑起来用Android Webview本地加载的时候报错：

  ```tex
  We‘re sorry but XXX doesn‘t work properly without JavaScript enabled. Please enable it to continue.
  ```

  这一看就是Webview没启用JavaScript？网上查了一下Webview的API，OK了。

  ```java
    @SuppressLint("SetJavaScriptEnabled")
      private void initWebView() {
          WebView webView = findViewById(R.id.pda_view);
          webView.loadUrl("http://172.20.10.3:8080/#/");
          webView.setWebChromeClient(new WebChromeClient() {
              @Override
              public void onProgressChanged(WebView view, int newProgress) {
                  super.onProgressChanged(view, newProgress);
                  if (newProgress >= 100) {
                      Log.d(TAG, "onProgressChanged: OK");
                  }
              }
          });
          WebSettings webSettings = webView.getSettings();
          webSettings.setJavaScriptEnabled(true);
          String cacheDirPath = SdApplication.getInstance().getFilesDir().getAbsolutePath() + "cache/";
          webSettings.setAppCachePath(cacheDirPath);
          // 1. 设置缓存路径

          webSettings.setAppCacheMaxSize(20 * 1024 * 1024);
          // 2. 设置缓存大小

          webSettings.setAppCacheEnabled(true);
      }

  ```

  ## 广播扫码

  这个是需要借助厂商提供内置的APP来配合完成，我去斑马官网找了相关文档。

  https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/

  这里有很详细的说明。

  需要在DataWedge中启用intent广播，然后自定义Action，在Android注册广播时带上设置的Action，就可以收到了。

  ```java
  package com.wistron_pda.broadcast;

  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.os.Bundle;
  import android.widget.EditText;

  import com.wistron_pda.R;
  import com.wistron_pda.core.SdApplication;

  public class ScanCodeBroadcastReceiver extends BroadcastReceiver {

      private OnReceiveCode onReceive;


      public interface OnReceiveCode {
          void ReceiveBarcodeCallback(String Code);
      }

      public void setOnReceive(OnReceiveCode onReceive) {
          this.onReceive = onReceive;
      }

      @Override
      public void onReceive(Context context, Intent intent) {

          String decodedSource = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_source));
          String decodedData = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_data));
          String decodedLabelType = intent.getStringExtra(SdApplication.getInstance().getString(R.string.datawedge_intent_key_label_type));

          onReceive.ReceiveBarcodeCallback(decodedData);
      }
  }

  ```

  一个简单的扫码回调接口。

  Activity实现广播类中提供的接口，实现。

  ```java
      private void initBroadcastReceiver() {

          setOnReceive = new ScanCodeBroadcastReceiver();
  //        IntentFilter intentFilter = new IntentFilter();
  //        intentFilter.addAction("com.android.scanservice.scancontext");
          IntentFilter filter = new IntentFilter();
          filter.addCategory(Intent.CATEGORY_DEFAULT);
          filter.addAction(getResources().getString(R.string.activity_intent_filter_action));
          registerReceiver(setOnReceive, filter);
          setOnReceive.setOnReceive(this);
      }


      @Override
      public void ReceiveBarcodeCallback(String Code) {
          Log.d(TAG, "条码：: " + Code);
      }
  ```

  初始化时将Action传进去，回调再配置一下，就可以了。

  activity_intent_filter_action 是 DataWedge 配置的Action。



  简单。

  ------

  https://www.cnblogs.com/yangchaojie/p/9660819.html



  https://techdocs.zebra.com/datawedge/7-6/guide/samples/basicintent1/


title: 西门子PLC数据读取 Observer设计模式
intro: 要监听值的变化，要定时读取值并回调，要比较值的大小，大于或小于并回调，动态订阅，并支持多台PLC设备，用JSON配置文件配置。
created_time: 2022-02-07 15:47:20.105585
html_text:
  <p>当我听到这个需求的时候，我差点爆粗口(实际上可能已经爆了，不过我忘了)。</p>
  <p>需求刚开始是：</p>
  <p>C#连接PLC Modbus读取值。</p>
  <p>我用C#写完了，觉得太简单了，还弄了个窗体。</p>
  <p>接着是：</p>
  <p>只读值那太简单了吧？这个不就一行代码的事情嘛，要监听值的变化并回调。</p>
  <p>好的，我花一下午弄了出来，并整合到.NET项目里面。</p>
  <p>又来了：</p>
  <p>要监听值的变化，要定时读取值并回调，要比较值的大小，大于或小于并回调，动态订阅，并支持多台PLC设备，用JSON配置文件配置。</p>
  <p><strong>FUCK！</strong></p>
  <p>那能咋办呢，做呗。</p>
  <h2 id="h2-u8BBEu8BA1u601Du8DEF"><a name="设计思路" class="reference-link"></a><span class="header-link octicon octicon-link"></span>设计思路</h2><p>首先想到的就是线程池和订阅模式。</p>
  <p>提供一个核心类，比如这个类叫做ModBusMonitor，在多台主机的情况下：</p>
  <p>ModBusMonitor plc1 = new ModBusMonitor(“PLC1”)</p>
  <p>ModBusMonitor plc2 = new ModBusMonitor(“PLC2”)</p>
  <p>这种方式可以保证多台主机的隔离。</p>
  <p>接着是订阅模式。</p>
  <p>ModBusMonitor对外提供订阅接口，定义一个Observer接口，创建多个Subscriber实现Observer：</p>
  <p>比如当值改变时订阅接口：ValueChangeSubscriber</p>
  <p>值比较时的订阅接口：ValueComparSubscriber</p>
  <p>同时每个订阅接口内部使用回调。</p>
  <p>因为我考虑的是，一台主机可以对N个地址进行N个不同的订阅，所以这是比较灵活的实现方式。</p>
  <p>所以回调隔离开，一个Subscriber一个Callback。</p>
  <p>ValueChangeSubscriber：</p>
  <pre><code class="lang-C#">    public class ValueChangeSubscriber : IObserver
      {

          public ushort Address;

          public IValueChangeCallBack callBack;

          public ValueChangeSubscriber(IValueChangeCallBack _callBack, ushort _address)
          {
              this.callBack = _callBack;
              this.Address = _address;
          }



          public void Reviced(string Name, ushort Address, ushort Value, bool ComparResult)
          {
          }

          public void Reviced(string Name, ushort Address, ushort NowValue)
          {
              callBack.OnChanged(Name, Address, NowValue);
          }
      }
  </code></pre>
  <p>ModBusMonitor实现：</p>
  <pre><code class="lang-c#">public class ModBusMonitor
      {
         private HashSet&lt;IObserver&gt; observers = new HashSet&lt;IObserver&gt;();

          private Dictionary&lt;string, ushort&gt; valueBuff = new Dictionary&lt;string, ushort&gt;();


          private string PLC_NAME;

          private Machine machine;

          private int READ_INTERVAL;

          private  int _IS_RUN = 1;

          private  ModbusTcpNet TCP_CLIENT = null;


          public ModBusMonitor(string pLC_NAME, int rEAD_INTERVAL)
          {
              PLC_NAME = pLC_NAME;
              READ_INTERVAL = rEAD_INTERVAL;
              //TODO check conn is already  exists  start thread to pool
              //
              machine = Global.GetMachine(PLC_NAME);

              try
              {
                  TCP_CLIENT = new ModbusTcpNet(machine.ip, machine.port);
                  OperateResult result = TCP_CLIENT.ConnectServer();
                  if (result.IsSuccess)
                  {
                    // TODO
                  }
                  else
                  {
                      throw new System.Exception(result.Message);
                  }
              }
              catch (SocketException SE)
              {
                  throw SE;
              }
          }

          public string GetKey(int address, object o) {
              string str = "{0}|{1}";
              if (o.GetType() == typeof(ValueChangeSubscriber))
              {
                  return String.Format(str, address, 0);
              }
              else if (o.GetType() == typeof(ValueComparSubscriber))
              {
                  return String.Format(str, address, 1);
              }
              else if (o.GetType() == typeof(ValueMonitorSubscriber))
              {
                  return String.Format(str, address, 2);
              }
              else {
                  throw new UnknownTypeException("未知类型");
              }
          }

          public void CancelSubscribe(IObserver observer) {
              observers.Remove(observer);
          }


          public void Subscribe(IObserver observer) {
              if (observer.GetType() == typeof(ValueChangeSubscriber))
              {
                  ValueChangeSubscriber subscriber = (ValueChangeSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueChangeTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else if (observer.GetType() == typeof(ValueComparSubscriber))
              {
                  ValueComparSubscriber subscriber = (ValueComparSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueComparTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else if (observer.GetType() == typeof(ValueMonitorSubscriber))
              {
                  ValueMonitorSubscriber subscriber = (ValueMonitorSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueMonitorTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else {
                  throw new UnknownTypeException("未知类型");
              }
          }

          public void ValueChangeTask(object obj) {
              ValueChangeSubscriber subscriber = obj as ValueChangeSubscriber;
              do
              {
                  Thread.Sleep(READ_INTERVAL);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  string key = GetKey(subscriber.Address, subscriber);
                  bool isExist = valueBuff.ContainsKey(key);
                  if (isExist)
                  {
                      ushort val = valueBuff[key];
                      if (val != newValue)
                      {
                          valueBuff[key] = newValue;
                          subscriber.Reviced(machine.name, subscriber.Address, newValue);
                      }
                  }
                  else {
                      valueBuff[key] = newValue;
                  }
              } while (_IS_RUN == 1&amp;&amp;observers.Contains(subscriber));
          }

          public void ValueComparTask(object obj) {
              ValueComparSubscriber subscriber = obj as ValueComparSubscriber;
              do
              {
                  Thread.Sleep(READ_INTERVAL);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  if (newValue &gt; subscriber.Value)
                  {
                      subscriber.Reviced(machine.name, subscriber.Address, newValue, true);
                  }
                  else {
                      subscriber.Reviced(machine.name, subscriber.Address, newValue, false);
                  }
              } while (_IS_RUN == 1 &amp;&amp; observers.Contains(subscriber));
          }

          public void ValueMonitorTask(object obj)
          {
              ValueMonitorSubscriber subscriber =  obj as ValueMonitorSubscriber;
              do
              {
                  Thread.Sleep(subscriber.Interval);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  subscriber.Reviced(machine.name, subscriber.Address, newValue);
              } while (_IS_RUN == 1 &amp;&amp; observers.Contains(subscriber));
          }



          public void Free() {
              _IS_RUN = 0;
              observers.Clear();
              TCP_CLIENT.Dispose();
          }

      }
  </code></pre>
  <p>每个Subscriber都是一个线程，提供取消订阅和释放主机。</p>
  <p>使用时：</p>
  <pre><code class="lang-c#">        static void Main(string[] args)
          {
              ModBusMonitor modbusMonitor = new ModBusMonitor("PLC1", 1000);
              ValueMonitorSubscriber v1 = new ValueMonitorSubscriber(new CallBack(), 1, 1000);
              modbusMonitor.Subscribe(v1);

              Thread.Sleep(2000);
              //modbusMonitor.CancelSubscribe(v1);
              modbusMonitor.Free();

          }
      }

      public class CallBack : IValueMonitorCallBack
      {
          public void OnBack(string name, int address, ushort val)
          {
              Console.WriteLine(val.ToString());
          }
      }

      public class CallBack1 : IValueChangeCallBack
      {
          public void OnChanged(string name, int address, ushort val)
          {
              Console.WriteLine("CALL BACK 1");
          }
      }
  </code></pre>
  <p>很完美，平平无奇却又有些精巧。</p>
  <p>:)</p>

markdown_text:
  当我听到这个需求的时候，我差点爆粗口(实际上可能已经爆了，不过我忘了)。

  需求刚开始是：

  C#连接PLC Modbus读取值。

  我用C#写完了，觉得太简单了，还弄了个窗体。

  接着是：

  只读值那太简单了吧？这个不就一行代码的事情嘛，要监听值的变化并回调。

  好的，我花一下午弄了出来，并整合到.NET项目里面。

  又来了：

  要监听值的变化，要定时读取值并回调，要比较值的大小，大于或小于并回调，动态订阅，并支持多台PLC设备，用JSON配置文件配置。

  **FUCK！**

  那能咋办呢，做呗。



  ## 设计思路

  首先想到的就是线程池和订阅模式。

  提供一个核心类，比如这个类叫做ModBusMonitor，在多台主机的情况下：

  ModBusMonitor plc1 = new ModBusMonitor("PLC1")

  ModBusMonitor plc2 = new ModBusMonitor("PLC2")

  这种方式可以保证多台主机的隔离。

  接着是订阅模式。

  ModBusMonitor对外提供订阅接口，定义一个Observer接口，创建多个Subscriber实现Observer：

  比如当值改变时订阅接口：ValueChangeSubscriber

  值比较时的订阅接口：ValueComparSubscriber

  同时每个订阅接口内部使用回调。

  因为我考虑的是，一台主机可以对N个地址进行N个不同的订阅，所以这是比较灵活的实现方式。

  所以回调隔离开，一个Subscriber一个Callback。

  ValueChangeSubscriber：

  ```C#
      public class ValueChangeSubscriber : IObserver
      {

          public ushort Address;

          public IValueChangeCallBack callBack;

          public ValueChangeSubscriber(IValueChangeCallBack _callBack, ushort _address)
          {
              this.callBack = _callBack;
              this.Address = _address;
          }



          public void Reviced(string Name, ushort Address, ushort Value, bool ComparResult)
          {
          }

          public void Reviced(string Name, ushort Address, ushort NowValue)
          {
              callBack.OnChanged(Name, Address, NowValue);
          }
      }
  ```



  ModBusMonitor实现：



  ```c#
  public class ModBusMonitor
      {
  	   private HashSet<IObserver> observers = new HashSet<IObserver>();

          private Dictionary<string, ushort> valueBuff = new Dictionary<string, ushort>();


          private string PLC_NAME;

          private Machine machine;

          private int READ_INTERVAL;

          private  int _IS_RUN = 1;

          private  ModbusTcpNet TCP_CLIENT = null;


          public ModBusMonitor(string pLC_NAME, int rEAD_INTERVAL)
          {
              PLC_NAME = pLC_NAME;
              READ_INTERVAL = rEAD_INTERVAL;
              //TODO check conn is already  exists  start thread to pool
              //
              machine = Global.GetMachine(PLC_NAME);

              try
              {
                  TCP_CLIENT = new ModbusTcpNet(machine.ip, machine.port);
                  OperateResult result = TCP_CLIENT.ConnectServer();
                  if (result.IsSuccess)
                  {
                    // TODO
                  }
                  else
                  {
                      throw new System.Exception(result.Message);
                  }
              }
              catch (SocketException SE)
              {
                  throw SE;
              }
          }

          public string GetKey(int address, object o) {
              string str = "{0}|{1}";
              if (o.GetType() == typeof(ValueChangeSubscriber))
              {
                  return String.Format(str, address, 0);
              }
              else if (o.GetType() == typeof(ValueComparSubscriber))
              {
                  return String.Format(str, address, 1);
              }
              else if (o.GetType() == typeof(ValueMonitorSubscriber))
              {
                  return String.Format(str, address, 2);
              }
              else {
                  throw new UnknownTypeException("未知类型");
              }
          }

          public void CancelSubscribe(IObserver observer) {
              observers.Remove(observer);
          }


          public void Subscribe(IObserver observer) {
              if (observer.GetType() == typeof(ValueChangeSubscriber))
              {
                  ValueChangeSubscriber subscriber = (ValueChangeSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueChangeTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else if (observer.GetType() == typeof(ValueComparSubscriber))
              {
                  ValueComparSubscriber subscriber = (ValueComparSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueComparTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else if (observer.GetType() == typeof(ValueMonitorSubscriber))
              {
                  ValueMonitorSubscriber subscriber = (ValueMonitorSubscriber)observer;
                  Thread thread = new Thread(new ParameterizedThreadStart(ValueMonitorTask));
                  thread.Start(subscriber);
                  observers.Add(subscriber);
              }
              else {
                  throw new UnknownTypeException("未知类型");
              }
          }

          public void ValueChangeTask(object obj) {
              ValueChangeSubscriber subscriber = obj as ValueChangeSubscriber;
              do
              {
                  Thread.Sleep(READ_INTERVAL);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  string key = GetKey(subscriber.Address, subscriber);
                  bool isExist = valueBuff.ContainsKey(key);
                  if (isExist)
                  {
                      ushort val = valueBuff[key];
                      if (val != newValue)
                      {
                          valueBuff[key] = newValue;
                          subscriber.Reviced(machine.name, subscriber.Address, newValue);
                      }
                  }
                  else {
                      valueBuff[key] = newValue;
                  }
              } while (_IS_RUN == 1&&observers.Contains(subscriber));
          }

          public void ValueComparTask(object obj) {
              ValueComparSubscriber subscriber = obj as ValueComparSubscriber;
              do
              {
                  Thread.Sleep(READ_INTERVAL);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  if (newValue > subscriber.Value)
                  {
                      subscriber.Reviced(machine.name, subscriber.Address, newValue, true);
                  }
                  else {
                      subscriber.Reviced(machine.name, subscriber.Address, newValue, false);
                  }
              } while (_IS_RUN == 1 && observers.Contains(subscriber));
          }

          public void ValueMonitorTask(object obj)
          {
              ValueMonitorSubscriber subscriber =  obj as ValueMonitorSubscriber;
              do
              {
                  Thread.Sleep(subscriber.Interval);
                  ushort newValue = TCP_CLIENT.ReadUInt16(String.Format("s={0};{1}", machine.sid, subscriber.Address)).Content;
                  subscriber.Reviced(machine.name, subscriber.Address, newValue);
              } while (_IS_RUN == 1 && observers.Contains(subscriber));
          }



          public void Free() {
              _IS_RUN = 0;
              observers.Clear();
              TCP_CLIENT.Dispose();
          }

      }
  ```



  每个Subscriber都是一个线程，提供取消订阅和释放主机。

  使用时：

  ```c#
          static void Main(string[] args)
          {
              ModBusMonitor modbusMonitor = new ModBusMonitor("PLC1", 1000);
              ValueMonitorSubscriber v1 = new ValueMonitorSubscriber(new CallBack(), 1, 1000);
              modbusMonitor.Subscribe(v1);
              
              Thread.Sleep(2000);
              //modbusMonitor.CancelSubscribe(v1);
              modbusMonitor.Free();
     
          }
      }

      public class CallBack : IValueMonitorCallBack
      {
          public void OnBack(string name, int address, ushort val)
          {
              Console.WriteLine(val.ToString());
          }
      }

      public class CallBack1 : IValueChangeCallBack
      {
          public void OnChanged(string name, int address, ushort val)
          {
              Console.WriteLine("CALL BACK 1");
          }
      }
  ```



  很完美，平平无奇却又有些精巧。



  :)


title: 纬创服务器项目部署操作文档
intro: 软件安装 纬创服务器无法接入互联网，如需安装软件包，前往：https://centos.pkgs.org/ 下载上传至服务器。 已安装运行环境说明 Nginx .....
created_time: 2022-02-17 06:13:15.486678
html_text:
  <h2 id="h2-u8F6Fu4EF6u5B89u88C5"><a name="软件安装" class="reference-link"></a><span class="header-link octicon octicon-link"></span>软件安装</h2><p>纬创服务器无法接入互联网，如需安装软件包，前往：<a href="https://centos.pkgs.org/">https://centos.pkgs.org/</a> 下载上传至服务器。</p>
  <h2 id="h2-u5DF2u5B89u88C5u8FD0u884Cu73AFu5883u8BF4u660E"><a name="已安装运行环境说明" class="reference-link"></a><span class="header-link octicon octicon-link"></span>已安装运行环境说明</h2><h3 id="h3-nginx"><a name="Nginx" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Nginx</h3><p>安装路径：/usr/local/nginx</p>
  <p>配置文件路径: /usr/local/nginx/conf</p>
  <p>配置文件修改后，运行：</p>
  <pre><code class="lang-bash">/usr/local/nginx/sbin/nginx -s reload
  </code></pre>
  <p>使其生效。</p>
  <h2 id="h2-rabbitmq"><a name="RabbitMQ" class="reference-link"></a><span class="header-link octicon octicon-link"></span>RabbitMQ</h2><p>安装路径：/usr/local/rabbitmq</p>
  <p>RabbitMQ常用命令：</p>
  <pre><code class="lang-bash">rabbitmqctl status //查看mq状态
  rabbitmq-server -detached //启动rabbitmq
  rebbitmq-stop //停止rabbitmq进程
  </code></pre>
  <p>查看rabbitmq是否启动：</p>
  <pre><code class="lang-bash">lsof -i:15672
  </code></pre>
  <p>15672端口被COMMAND进程使用表示启动</p>
  <h2 id="h2-dotnet-"><a name="Dotnet 项目运行" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Dotnet 项目运行</h2><p>纬创项目路径：/opt/wistron/wistron-web-publish</p>
  <p>运行项目：</p>
  <pre><code class="lang-bash">/opt/wistron/start.sh
  </code></pre>
  <p>运行日志输出到 /opt/wistron/nohub.out 文件</p>
  <p>实时查看运行日志：</p>
  <pre><code class="lang-bash">tail -f nohub.out
  </code></pre>
  <p>日志查找：</p>
  <pre><code class="lang-bash">grep 'PLC' nohub.out //查询输出nohub.out日志文件中有关PLC字符串的信息
  </code></pre>
  <p>查看项目是否运行：</p>
  <pre><code class="lang-bash">lsof -i:5000
  </code></pre>
  <p>查看5000端口是否在使用或者查看nohub.out文件是否在实时输出日志。</p>
  <h2 id="h2-u4E0Au4F20u9879u76EEu81F3u670Du52A1u5668u5E76u90E8u7F72"><a name="上传项目至服务器并部署" class="reference-link"></a><span class="header-link octicon octicon-link"></span>上传项目至服务器并部署</h2><h3 id="h3-xshell"><a name="Xshell" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Xshell</h3><p>服务器已安装lrzsz工具包，进入目录下输入 <strong>rz</strong> 即可打开windows文件选择器，选择需要上传的文件即可，文件将会上传至当前linux终端所在路径。</p>
  <p>使用 <strong>sz</strong> 文件名将会打开windows文件选择器，选择保存目录即可将文件下载至windows本机。</p>
  <h4 id="h4-u624Bu52A8u90E8u7F72"><a name="手动部署" class="reference-link"></a><span class="header-link octicon octicon-link"></span>手动部署</h4><p>进入 /opt/wistron</p>
  <pre><code class="lang-bash">lsof -i:5000 //查看pid
  kill -9 pid //结束项目进程
  rm -rf wistron-web-piblish //删除项目文件夹
  unzip wistron-web-piblish.zip //解压上传的项目压缩包
  ./start.sh //启动项目
  tail -f nohub.out // 查看项目是否成功运行
  </code></pre>
  <h4 id="h4-u81EAu52A8u90E8u7F72"><a name="自动部署" class="reference-link"></a><span class="header-link octicon octicon-link"></span>自动部署</h4><p>进入 /opt/wistron</p>
  <p>将wistron-web-piblish.zip 上传至 /opt/wistron</p>
  <p>./start.sh </p>
  <p>即可完成自动部署。</p>
  <h2 id="h2-scp"><a name="SCP" class="reference-link"></a><span class="header-link octicon octicon-link"></span>SCP</h2><p>打开windows命令行：</p>
  <pre><code class="lang-bash">scp -r -P 40001 E:\Project\Wistron\wistron-vue\dist\* root@10.41.241.29:/opt/wistron/vue
  </code></pre>
  <h2 id="h2-vue-"><a name="Vue项目" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Vue项目</h2><p>路径：/opt/wistron/vue</p>
  <h2 id="h2-start-sh"><a name="Start.sh" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Start.sh</h2><pre><code class="lang-shell">#!/bin/bash

  rm -rf /opt/wistron/wistron-web-publish

  if [ ! -f "/opt/wistron/wistron-web-publish.zip" ];then
      echo "zip文件不存在"
  else    
      echo "===================开始部署纬创项目================"
      pgrep dotnet | xargs kill -s 9
      rm -rf /opt/wistron/wistron-web-publish
      unzip /opt/wistron/wistron-web-publish.zip
      nohup dotnet /opt/wistron/wistron-web-publish/SD.Wistron.WebApi.dll  &amp;
  fi
  echo "===================部署成功!================"
  </code></pre>
  <h2 id="h2-mysql"><a name="MySql" class="reference-link"></a><span class="header-link octicon octicon-link"></span>MySql</h2><p>进入MySql</p>
  <pre><code class="lang-bash">mysql -uss -p
  </code></pre>
  <p>输入密码：2CFXyJcU<a href="https://github.com/Ks0" title="@Ks0" class="at-link">@Ks0</a></p>
  <p>quit 退出。</p>
  <h2 id="h2-u5E38u7528u547Du4EE4"><a name="常用命令" class="reference-link"></a><span class="header-link octicon octicon-link"></span>常用命令</h2><pre><code class="lang-bash">find / -name '' //查找文件
  ./configuare --prefix=路径 //编译软件包 make &amp;&amp; make install
  systemctl status xxx.service //查看服务运行状态 start stop
  firewall-cmd --zone=public --list-ports //查看所有开放端口
  firewall-cmd --zone=public --add-port=80/tcp --permanent //开放80端口
  firewall-cmd --reload //重载生效
  lsof -i:端口号 //查看端口使用情况
  find / -not -name 'xx' -exec rm -rf {} /; //删除当前除了该文件的所有文件
  pgrep 名称 | xargs kill -s 9 //查找所有进程名称并全部杀死
  rpm -ivh rpm文件名称 //安装rpm软件包
  rpm -qa | grep 名称 //根据名称查找已安装的软件包
  fdisk - //查看磁盘 带*可以使用 mnt 命令挂载 example: mnt /dev/scdd /mnt/usb
  rm -rf ./* //删除当前目录所有文件
  vi 文件名 //编辑文件 在vi中按i进入编辑模式，esc退出，输入:号，/查找 wq保存 q!强制退出不保存
  cp //复制
  mv //移动
  ls //列出所有文件
  touch 创建文件
  mkdir 创建文件夹
  tar -zxvf xxx.tar.gz //解压tar.gz
  vi /etc/profile 编辑系统环境变量 source /etc/profile 生效
  chmod +x 文件|文件夹 //提权
  </code></pre>

markdown_text:
  ## 软件安装

  纬创服务器无法接入互联网，如需安装软件包，前往：https://centos.pkgs.org/ 下载上传至服务器。

  ## 已安装运行环境说明

  ### Nginx

  安装路径：/usr/local/nginx

  配置文件路径: /usr/local/nginx/conf

  配置文件修改后，运行：

  ```bash
  /usr/local/nginx/sbin/nginx -s reload
  ```

  使其生效。

  ## RabbitMQ

  安装路径：/usr/local/rabbitmq

  RabbitMQ常用命令：

  ```bash
  rabbitmqctl status //查看mq状态
  rabbitmq-server -detached //启动rabbitmq
  rebbitmq-stop //停止rabbitmq进程
  ```

  查看rabbitmq是否启动：

  ```bash
  lsof -i:15672
  ```

  15672端口被COMMAND进程使用表示启动

  ## Dotnet 项目运行

  纬创项目路径：/opt/wistron/wistron-web-publish

  运行项目：

  ```bash
  /opt/wistron/start.sh
  ```

  运行日志输出到 /opt/wistron/nohub.out 文件

  实时查看运行日志：

  ```bash
  tail -f nohub.out
  ```

  日志查找：

  ```bash
  grep 'PLC' nohub.out //查询输出nohub.out日志文件中有关PLC字符串的信息 
  ```

  查看项目是否运行：

  ```bash
  lsof -i:5000
  ```

  查看5000端口是否在使用或者查看nohub.out文件是否在实时输出日志。



  ## 上传项目至服务器并部署

  ### Xshell

  服务器已安装lrzsz工具包，进入目录下输入 **rz** 即可打开windows文件选择器，选择需要上传的文件即可，文件将会上传至当前linux终端所在路径。

  使用 **sz** 文件名将会打开windows文件选择器，选择保存目录即可将文件下载至windows本机。

  #### 手动部署

  进入 /opt/wistron

  ```bash
  lsof -i:5000 //查看pid
  kill -9 pid //结束项目进程
  rm -rf wistron-web-piblish //删除项目文件夹
  unzip wistron-web-piblish.zip //解压上传的项目压缩包
  ./start.sh //启动项目
  tail -f nohub.out // 查看项目是否成功运行
  ```

  #### 自动部署

  进入 /opt/wistron

  将wistron-web-piblish.zip 上传至 /opt/wistron

  ./start.sh 

  即可完成自动部署。

  ## SCP

  打开windows命令行：

  ```bash
  scp -r -P 40001 E:\Project\Wistron\wistron-vue\dist\* root@10.41.241.29:/opt/wistron/vue 
  ```



  ## Vue项目

  路径：/opt/wistron/vue



  ## Start.sh

  ```shell
  #!/bin/bash

  rm -rf /opt/wistron/wistron-web-publish

  if [ ! -f "/opt/wistron/wistron-web-publish.zip" ];then
  	echo "zip文件不存在"
  else	
  	echo "===================开始部署纬创项目================"
  	pgrep dotnet | xargs kill -s 9
  	rm -rf /opt/wistron/wistron-web-publish
  	unzip /opt/wistron/wistron-web-publish.zip
  	nohup dotnet /opt/wistron/wistron-web-publish/SD.Wistron.WebApi.dll  &
  fi
  echo "===================部署成功!================"

  ```



  ## MySql

  进入MySql

  ```bash
  mysql -uss -p
  ```

  输入密码：2CFXyJcU@Ks0

  quit 退出。



  ## 常用命令

  ```bash
  find / -name '' //查找文件
  ./configuare --prefix=路径 //编译软件包 make && make install
  systemctl status xxx.service //查看服务运行状态 start stop
  firewall-cmd --zone=public --list-ports //查看所有开放端口
  firewall-cmd --zone=public --add-port=80/tcp --permanent //开放80端口
  firewall-cmd --reload //重载生效
  lsof -i:端口号 //查看端口使用情况
  find / -not -name 'xx' -exec rm -rf {} /; //删除当前除了该文件的所有文件
  pgrep 名称 | xargs kill -s 9 //查找所有进程名称并全部杀死
  rpm -ivh rpm文件名称 //安装rpm软件包
  rpm -qa | grep 名称 //根据名称查找已安装的软件包
  fdisk - //查看磁盘 带*可以使用 mnt 命令挂载 example: mnt /dev/scdd /mnt/usb
  rm -rf ./* //删除当前目录所有文件
  vi 文件名 //编辑文件 在vi中按i进入编辑模式，esc退出，输入:号，/查找 wq保存 q!强制退出不保存
  cp //复制
  mv //移动
  ls //列出所有文件
  touch 创建文件
  mkdir 创建文件夹
  tar -zxvf xxx.tar.gz //解压tar.gz
  vi /etc/profile 编辑系统环境变量 source /etc/profile 生效
  chmod +x 文件|文件夹 //提权
  ```


title: 求和算法
intro: 给定一个数组，一个目标值，算出所有和等于目标值的组合，组合中会出现重复值，且每个重复值只能在每个组合出现一次。包含小数。
created_time: 2022-03-29 05:34:23.345410
html_text:
  <h3 id="h3-u8981u6C42"><a name="要求" class="reference-link"></a><span class="header-link octicon octicon-link"></span>要求</h3><p>给定一个数组，一个目标值，算出所有和等于目标值的组合，组合中会出现重复值，且每个重复值只能在每个组合出现一次。</p>
  <p>包含小数。</p>
  <pre><code class="lang-java">import java.util.*;
  import java.util.stream.Collectors;


  public class Sum {

      public static void main(String[] args) {

          Double[] nums = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 5.5,0.1,  0.4, 0.5, 0.5,0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,};

          List&lt;Double&gt; nlist = Arrays.asList(nums);
          Double target = 3.5;

          List&lt;List&lt;Double&gt;&gt; result = sum(nlist, target, null);
          for (List&lt;Double&gt; doubles : result) {
              System.out.println(Arrays.toString(new List[]{doubles}));
          }
      }


      public static List&lt;List&lt;Double&gt;&gt; sum(List&lt;Double&gt; nums, Double target, List&lt;List&lt;Double&gt;&gt; currentMatch) {
          int len = nums.size();
          if (Objects.isNull(currentMatch)) currentMatch = new ArrayList&lt;&gt;();

          for (int i = 0; i &lt; len; i++) {
              if (target &lt; nums.get(i)) continue;
              for (int j = i + 1; j &lt; nums.size(); j++) {
                  if (target &lt; nums.get(j)) continue;
                  if (nums.get(i) + nums.get(j) == target) {
                      currentMatch.add(Arrays.asList(nums.get(i), nums.get(j)));
                  } else if ((nums.get(i) + nums.get(j)) &lt; target) {
                      List&lt;Double&gt; p1 = new ArrayList&lt;&gt;();
                      p1.add(nums.get(i));
                      p1.add(nums.get(j));
                      List&lt;Double&gt; tempNums = new ArrayList&lt;&gt;(nums);
                      getMatch(p1, tempNums, j + 1, target, currentMatch, null,null);
                  }
              }
          }
          return currentMatch;
      }


      public static void getMatch(List&lt;Double&gt; p1, List&lt;Double&gt; nums, int i, Double target, List&lt;List&lt;Double&gt;&gt; currentMatch, List&lt;Integer&gt; exclude,List&lt;Integer&gt; skips) {
          if (Objects.isNull(exclude)) exclude = new ArrayList&lt;&gt;();
          if (Objects.isNull(skips)) skips = new ArrayList&lt;&gt;();
          int len = nums.size();
          for (; i &lt; len; i++) {
              int finalI = i;
              if (exclude.stream().anyMatch((e) -&gt; e == finalI))continue;
              if (skips.stream().anyMatch((e) -&gt; e == finalI)) continue;
              if ((getSum(p1) + nums.get(i)) == target) {
                  p1.add(nums.get(i));
                  currentMatch.add(p1);
                  exclude.clear();
                  skips.clear();
              } else if ((getSum(p1) + nums.get(i)) &lt; target) {
                  p1.add(nums.get(i));
                  exclude.add(i);
                  getMatch(p1, nums, 0, target, currentMatch, exclude,skips);
              }else if ((getSum(p1) + nums.get(i)) &gt; target) {
                  skips.add(i);
                  getMatch(p1, nums, 0, target, currentMatch, exclude,skips);
              }
          }
      }


      static Double getSum(List&lt;Double&gt; r) {
          if (r.isEmpty()) {
              return 0.0;
          } else {
              return r.stream().mapToDouble(Double::doubleValue).sum();
          }
      }
  }
  </code></pre>
  <p>大概思路是，<strong>O(n^2)</strong> 循环内先看前后两数的和是否为目标值，如果这两个数相加小于目标值，那么进入递归算出组合。</p>
  <p>将已选出的组合数字在递归循环中排除。</p>
  <p>可以确保值只被使用一次。</p>
  <p>如果追加数字大于目标值，将该数下表记录，将下次递归循环重置，遇到该下标跳过。</p>
  <p>优化空间还是有的，只不过我懒得看了。</p>

markdown_text:
  ### 要求

  给定一个数组，一个目标值，算出所有和等于目标值的组合，组合中会出现重复值，且每个重复值只能在每个组合出现一次。

  包含小数。



  ```java
  import java.util.*;
  import java.util.stream.Collectors;


  public class Sum {

      public static void main(String[] args) {

          Double[] nums = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 5.5,0.1,  0.4, 0.5, 0.5,0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,};

          List<Double> nlist = Arrays.asList(nums);
          Double target = 3.5;

          List<List<Double>> result = sum(nlist, target, null);
          for (List<Double> doubles : result) {
              System.out.println(Arrays.toString(new List[]{doubles}));
          }
      }


      public static List<List<Double>> sum(List<Double> nums, Double target, List<List<Double>> currentMatch) {
          int len = nums.size();
          if (Objects.isNull(currentMatch)) currentMatch = new ArrayList<>();

          for (int i = 0; i < len; i++) {
              if (target < nums.get(i)) continue;
              for (int j = i + 1; j < nums.size(); j++) {
                  if (target < nums.get(j)) continue;
                  if (nums.get(i) + nums.get(j) == target) {
                      currentMatch.add(Arrays.asList(nums.get(i), nums.get(j)));
                  } else if ((nums.get(i) + nums.get(j)) < target) {
                      List<Double> p1 = new ArrayList<>();
                      p1.add(nums.get(i));
                      p1.add(nums.get(j));
                      List<Double> tempNums = new ArrayList<>(nums);
                      getMatch(p1, tempNums, j + 1, target, currentMatch, null,null);
                  }
              }
          }
          return currentMatch;
      }


      public static void getMatch(List<Double> p1, List<Double> nums, int i, Double target, List<List<Double>> currentMatch, List<Integer> exclude,List<Integer> skips) {
          if (Objects.isNull(exclude)) exclude = new ArrayList<>();
          if (Objects.isNull(skips)) skips = new ArrayList<>();
          int len = nums.size();
          for (; i < len; i++) {
              int finalI = i;
              if (exclude.stream().anyMatch((e) -> e == finalI))continue;
              if (skips.stream().anyMatch((e) -> e == finalI)) continue;
              if ((getSum(p1) + nums.get(i)) == target) {
                  p1.add(nums.get(i));
                  currentMatch.add(p1);
                  exclude.clear();
                  skips.clear();
              } else if ((getSum(p1) + nums.get(i)) < target) {
                  p1.add(nums.get(i));
                  exclude.add(i);
                  getMatch(p1, nums, 0, target, currentMatch, exclude,skips);
              }else if ((getSum(p1) + nums.get(i)) > target) {
                  skips.add(i);
                  getMatch(p1, nums, 0, target, currentMatch, exclude,skips);
              }
          }
      }


      static Double getSum(List<Double> r) {
          if (r.isEmpty()) {
              return 0.0;
          } else {
              return r.stream().mapToDouble(Double::doubleValue).sum();
          }
      }
  }
  ```



  大概思路是，**O(n^2)** 循环内先看前后两数的和是否为目标值，如果这两个数相加小于目标值，那么进入递归算出组合。

  将已选出的组合数字在递归循环中排除。

  可以确保值只被使用一次。

  如果追加数字大于目标值，将该数下表记录，将下次递归循环重置，遇到该下标跳过。

  优化空间还是有的，只不过我懒得看了。


title: Vue 和 Zebra 打印机连接直接打印条码
created_time: 2022-04-21 09:41:34.023366
intro:
  首先，Vue是无法调用Windows系统功能的，那么打印只能通过浏览器的打印功能来实现，这样显然不行，效率太低，而且斑马打印机是通过ZPL指令进行打印的，用浏览器打印非常不方便。  两个办法：  1. 换技术，使用能够调用Windows Api的语言进行开发功能。 2. 转手给后台程序打印。  我继续用Vue，用C#写了一个后台打印程序，C#开启websocket服务，Vue连接websocket进行通讯，如果Vue接收到ZPL指令就发送后台打印程序，由后台打印程序完成条码打印。

html_text:
  <p>首先，Vue是无法调用Windows系统功能的，那么打印只能通过浏览器的打印功能来实现，这样显然不行，效率太低，而且斑马打印机是通过ZPL指令进行打印的，用浏览器打印非常不方便。</p>
  <p>两个办法：</p>
  <ol>
  <li>换技术，使用能够调用Windows Api的语言进行开发功能。</li><li>转手给后台程序打印。</li></ol>
  <p>我继续用Vue，用C#写了一个后台打印程序，C#开启websocket服务，Vue连接websocket进行通讯，如果Vue接收到ZPL指令就发送后台打印程序，由后台打印程序完成条码打印。</p>
  <p>后台打印程序Websocket，保持心跳。</p>
  <pre><code class="lang-c#">using Fleck;
  using log4net;
  using System;
  using System.Collections.Generic;
  using System.Text;

  namespace CiemisPrintService
  {
      class WebSocketBootStrap
      {

          public static void Start()
          {
              ILog logger = LogManager.GetLogger(typeof(FleckLog));

              FleckLog.LogAction = (level, message, ex) =&gt;
              {
                  switch (level)
                  {
                      case LogLevel.Debug:
                          SimpleLogHelper.LogInfo(message);
                          break;
                      case LogLevel.Error:
                          SimpleLogHelper.LogError(message);
                          break;
                      case LogLevel.Warn:
                          SimpleLogHelper.LogError(message);
                          break;
                      default:
                          SimpleLogHelper.LogInfoSuccess(message);
                          break;
                  }
              };

              var server = new WebSocketServer("ws://0.0.0.0:8181");
              server.SupportedSubProtocols = new[] { "v10.stomp", "v11.stomp" };
              server.RestartAfterListenError = true;
              server.Start(socket =&gt;
              {
                  socket.OnOpen = () =&gt;
                  {
                      string clientUrl = socket.ConnectionInfo.ClientIpAddress + ":" + socket.ConnectionInfo.ClientPort;
                      SimpleLogHelper.LogInfoSuccess("连接进入:"+ clientUrl);
                  };
                  socket.OnClose = () =&gt; SimpleLogHelper.LogError("服务关闭!"); ;
                  socket.OnMessage = message =&gt; {
                      if (message.Equals("ping"))
                      {
                          socket.Send("pong");
                      }
                      else {
                          ZebraLibrary.ZebraPrintHelper.PrinterType = ZebraLibrary.DeviceType.DRV;
                          ZebraLibrary.ZebraPrintHelper.PrinterName = MainForm.mainWindow.GetDefaultPrinterName();
                          ZebraLibrary.ZebraPrintHelper.PrintCommand(message);
                      }
                      SimpleLogHelper.LogInfoSuccess(message);
                  };
                  socket.OnError = e =&gt;
                  {
                      SimpleLogHelper.LogError(e.Message);
                  };
                  socket.OnPing = ping =&gt;
                  {
                      socket.SendPong(Encoding.Default.GetBytes("pong"));
                      SimpleLogHelper.LogInfoSuccess(Encoding.Default.GetString(ping));
                  };

              });

          }

      }
  }
  </code></pre>
  <p>代码很少，很简单的功能。</p>
  <p>Vue Socket进行连接。</p>
  <pre><code class="lang-js">  initSocket () {
        // 实例化socket
        // 监听socket连接
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
        this.socket.onclose = this.close
      },
      reInitSocket () {
        // 实例化socket
        // 监听socket连接
        this.socket = new WebSocket(MQTT_SERVICE)
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
        this.socket.onclose = this.close
      },
      countDownChanged (dismissCountDown) {
        this.dismissCountDown = dismissCountDown
      },
      showAlert () {
        this.dismissCountDown = this.dismissSecs
      },
      keepAlive () {
        var timeout = 1000
        if (this.socket.readyState === WebSocket.OPEN) {
          this.$store.commit('PING', true)
          this.socket.send('ping')
        }
        this.timeId = setTimeout(this.keepAlive, timeout)
      },
      cancelKeepAlive () {
        if (this.timeId) {
          clearTimeout(this.timeId)
        }
      },
      open: function () {
        console.log('socket连接成功')
        this.$store.commit('PING', true)
        this.$global.setWs(this.socket)
        JSON.parse(localStorage.getItem('zpl')).forEach((item, i) =&gt; {
          this.socket.send(item)
        })
        localStorage.setItem('zpl', '')
        this.keepAlive()
      },
      error: function () {
        console.log('连接错误')
      },
      getMessage: function (msg) {
      },
      send: function (msg) {
        this.socket.send(msg)
      },
      close: function () {
        window._VMA.$emit('PRINTER_ERROR', {
          time: 0,
          show: true,
          text: '打印机已断开连接',
          type: 'danger'
        })
        this.$store.commit('PING', false)
        console.log('socket已经关闭')
        setTimeout(() =&gt; {
          this.reInitSocket()
        }, 5000)
      }
    }
  </code></pre>

markdown_text:
  首先，Vue是无法调用Windows系统功能的，那么打印只能通过浏览器的打印功能来实现，这样显然不行，效率太低，而且斑马打印机是通过ZPL指令进行打印的，用浏览器打印非常不方便。

  两个办法：

  1. 换技术，使用能够调用Windows Api的语言进行开发功能。
  2. 转手给后台程序打印。

  我继续用Vue，用C#写了一个后台打印程序，C#开启websocket服务，Vue连接websocket进行通讯，如果Vue接收到ZPL指令就发送后台打印程序，由后台打印程序完成条码打印。

  后台打印程序Websocket，保持心跳。

  ```c#
  using Fleck;
  using log4net;
  using System;
  using System.Collections.Generic;
  using System.Text;

  namespace CiemisPrintService
  {
      class WebSocketBootStrap
      {

          public static void Start()
          {
              ILog logger = LogManager.GetLogger(typeof(FleckLog));

              FleckLog.LogAction = (level, message, ex) =>
              {
                  switch (level)
                  {
                      case LogLevel.Debug:
                          SimpleLogHelper.LogInfo(message);
                          break;
                      case LogLevel.Error:
                          SimpleLogHelper.LogError(message);
                          break;
                      case LogLevel.Warn:
                          SimpleLogHelper.LogError(message);
                          break;
                      default:
                          SimpleLogHelper.LogInfoSuccess(message);
                          break;
                  }
              };

              var server = new WebSocketServer("ws://0.0.0.0:8181");
              server.SupportedSubProtocols = new[] { "v10.stomp", "v11.stomp" };
              server.RestartAfterListenError = true;
              server.Start(socket =>
              {
                  socket.OnOpen = () =>
                  {
                      string clientUrl = socket.ConnectionInfo.ClientIpAddress + ":" + socket.ConnectionInfo.ClientPort;
                      SimpleLogHelper.LogInfoSuccess("连接进入:"+ clientUrl);
                  };
                  socket.OnClose = () => SimpleLogHelper.LogError("服务关闭!"); ;
                  socket.OnMessage = message => {
                      if (message.Equals("ping"))
                      {
                          socket.Send("pong");
                      }
                      else {
                          ZebraLibrary.ZebraPrintHelper.PrinterType = ZebraLibrary.DeviceType.DRV;
                          ZebraLibrary.ZebraPrintHelper.PrinterName = MainForm.mainWindow.GetDefaultPrinterName();
                          ZebraLibrary.ZebraPrintHelper.PrintCommand(message);
                      }
                      SimpleLogHelper.LogInfoSuccess(message);
                  };
                  socket.OnError = e =>
                  {
                      SimpleLogHelper.LogError(e.Message);
                  };
                  socket.OnPing = ping =>
                  {
                      socket.SendPong(Encoding.Default.GetBytes("pong"));
                      SimpleLogHelper.LogInfoSuccess(Encoding.Default.GetString(ping));
                  };

              });

          }

      }
  }
  ```

  代码很少，很简单的功能。

  Vue Socket进行连接。

  ```js
    initSocket () {
        // 实例化socket
        // 监听socket连接
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
        this.socket.onclose = this.close
      },
      reInitSocket () {
        // 实例化socket
        // 监听socket连接
        this.socket = new WebSocket(MQTT_SERVICE)
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
        this.socket.onclose = this.close
      },
      countDownChanged (dismissCountDown) {
        this.dismissCountDown = dismissCountDown
      },
      showAlert () {
        this.dismissCountDown = this.dismissSecs
      },
      keepAlive () {
        var timeout = 1000
        if (this.socket.readyState === WebSocket.OPEN) {
          this.$store.commit('PING', true)
          this.socket.send('ping')
        }
        this.timeId = setTimeout(this.keepAlive, timeout)
      },
      cancelKeepAlive () {
        if (this.timeId) {
          clearTimeout(this.timeId)
        }
      },
      open: function () {
        console.log('socket连接成功')
        this.$store.commit('PING', true)
        this.$global.setWs(this.socket)
        JSON.parse(localStorage.getItem('zpl')).forEach((item, i) => {
          this.socket.send(item)
        })
        localStorage.setItem('zpl', '')
        this.keepAlive()
      },
      error: function () {
        console.log('连接错误')
      },
      getMessage: function (msg) {
      },
      send: function (msg) {
        this.socket.send(msg)
      },
      close: function () {
        window._VMA.$emit('PRINTER_ERROR', {
          time: 0,
          show: true,
          text: '打印机已断开连接',
          type: 'danger'
        })
        this.$store.commit('PING', false)
        console.log('socket已经关闭')
        setTimeout(() => {
          this.reInitSocket()
        }, 5000)
      }
    }
  ```


title: 蓝奏云下载器
intro: 蓝奏云下载器
created_time: 2022-04-27 15:48:09.049788
html_text:
  <h2 id="h2--"><a name="使用说明：" class="reference-link"></a><span class="header-link octicon octicon-link"></span>使用说明：</h2><p>打开exe文件：<br><img src="/upload/files/png/c59028dc532bf572d0d4c0d6e7eba123.png" alt=""><br>输入保存的路径<br><img src="/upload/files/png/fc4f4d0459c5b30892f1beb5da618e68.png" alt=""><br>是否解压输入大写Y，之后会将下载的资源自动解压缩
  <p>回车后即可开始下载资源。</p>
  <p>软件下载地址：<br><a href="https://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/%E8%93%9D%E5%A5%8F%E4%BA%91%E4%B8%8B%E8%BD%BD%E5%99%A8.exe">https://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/%E8%93%9D%E5%A5%8F%E4%BA%91%E4%B8%8B%E8%BD%BD%E5%99%A8.exe</a></p>

markdown_text:
  ## 使用说明：
  打开exe文件：
  ![](/upload/files/png/c59028dc532bf572d0d4c0d6e7eba123.png)
  输入保存的路径
  ![](/upload/files/png/fc4f4d0459c5b30892f1beb5da618e68.png)
  是否解压输入大写Y，之后会将下载的资源自动解压缩

  回车后即可开始下载资源。

  软件下载地址：
  https://s3.z100.vip:28592/%E7%A8%8B%E5%BA%8F/%E8%93%9D%E5%A5%8F%E4%BA%91%E4%B8%8B%E8%BD%BD%E5%99%A8.exe


title: Angular8开发基于 PrimeNG 框架的后台管理 --持续更新
intro: 再也不想看Vue像屎一样的文件结构了，看看angular。
created_time: 2022-04-29 16:59:50.377018
html_text:
  <h2 id="h2-requirement"><a name="requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>requirement</h2><p>安装angular</p>
  <p><code>npm install -g angular<a href="https://github.com/cli_" title="@cli_" class="at-link">@cli_</a>#_&#64;_#_8.3.29</code></p>
  <p>创建angular项目</p>
  <p><code>ng new you project</code></p>
  <p>加入PrimeNG框架支持</p>
  <p><code>npm install primeng --save</code></p>
  <p><code>npm install primeicons --save</code></p>
  <p>angular.json 加入style</p>
  <pre><code class="lang-json">  "styles": [
                "src/styles.css",
                "node_modules/primeicons/primeicons.css",
                "node_modules/primeng/resources/primeng.min.css"
              ],
  </code></pre>
  <h2 id="h2-u591Au8BEDu8A00u652Fu6301"><a name="多语言支持" class="reference-link"></a><span class="header-link octicon octicon-link"></span>多语言支持</h2><p><code>npm install --save <a href="https://github.com/ngx" title="@ngx" class="at-link">@ngx</a>-translate</code></p>
  <p>app.module.ts 加入代码：</p>
  <pre><code class="lang-typescript">import { HttpClientModule, HttpClient } from '@angular/common/http';
  import { NgModule } from '@angular/core';
  import { BrowserModule } from '@angular/platform-browser';
  import { Route, RouterModule, Routes } from '@angular/router';
  import { TranslateModule, TranslateLoader } from '@ngx-translate/core';

  import { AppRoutingModule } from './app-routing.module';
  import { AppComponent } from './app.component';
  import { TranslateHttpLoader } from '@ngx-translate/http-loader';
  import { MainLayoutComponent } from './view/layout/main-layout/main-layout.component';
  import { SharedModule } from './shared/shared/shared.module';
  import { HelloWorldComponent } from './view/pages/hello-world/hello-world.component';

  export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
  }

  @NgModule({
    declarations: [AppComponent, MainLayoutComponent, HelloWorldComponent],
    imports: [
      BrowserModule,
      AppRoutingModule,
      SharedModule,
      RouterModule,
      HttpClientModule,
      // 在这里配置
      TranslateModule.forRoot({
        loader: {
          provide: TranslateLoader,
          useFactory: HttpLoaderFactory,
          deps: [HttpClient],
        },
      }),
    ],
    providers: [],
    bootstrap: [AppComponent],
  })
  export class AppModule {}
  </code></pre>
  <p>assets创建i18n文件夹，里面放入语言json文件。</p>
  <p>app.component.ts</p>
  <pre><code class="lang-typescript">import { Component } from '@angular/core';
  import { TranslateService } from '@ngx-translate/core';
  import { PrimeNGConfig } from 'primeng/api';

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
  })
  export class AppComponent {
    constructor(
      private primengConfig: PrimeNGConfig,
      private translateService: TranslateService
    ) {}

    ngOnInit() {
      this.primengConfig.ripple = true;
      this.translateService.setDefaultLang('zh');
    }
    ngAfterViewInit() {
       console.log('lang ');
       this.translateService.use('zh');
       this.translateService.get('xxx')
        .subscribe(res =&gt; this.primengConfig.setTranslation(res));
    }
    translate(lang: string) {
      console.log(lang);
      this.translateService.use(lang);
      this.translateService
       .get('xxx')
       .subscribe(res =&gt; this.primengConfig.setTranslation(res));
    }
  }
  </code></pre>
  <h2 id="h2-route"><a name="Route" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Route</h2><pre><code class="lang-typescript">import { HelloWorldComponent } from './view/pages/hello-world/hello-world.component';
  /*
   * @Author: C.J
   * @Date: 2022-04-29 23:00:55
   * @Last Modified by: C.J
   * @Last Modified time: 2022-04-30 00:16:46
   */
  import { NgModule } from '@angular/core';
  import { RouterModule, Routes } from '@angular/router';
  import { AppMainComponent } from './app.main.component';

  const routes: Routes = [
    {
      path: '', component: AppMainComponent,
      children: [
        { path: '', component: HelloWorldComponent }
      ]
    },
  ]

  @NgModule({
    imports: [RouterModule.forRoot(routes,  {scrollPositionRestoration: 'enabled', anchorScrolling:'enabled'}
    )],
    exports: [RouterModule]
  })
  export class AppRoutingModule { }
  </code></pre>
  <h2 id="h2-u540Eu53F0u754Cu9762"><a name="后台界面" class="reference-link"></a><span class="header-link octicon octicon-link"></span>后台界面</h2><p><img src="/upload/files/png/03c316f11a756ba353e5ca078740fe07.png" alt="">
  <p>调了一天CSS，MLGB。</p>
  <h2 id="h2-tabview-"><a name="Tabview标签页" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Tabview标签页</h2><pre><code class="lang-typescript">export class AppTabViewComponent implements AfterViewInit, OnInit {

    closable: boolean = true

    model: any[];

    activeIndex = 0

    @Input()
    tabs: MenuItem[] = [];

    menuSourceSubscription: Subscription;


    constructor(public appMain: AppMainComponent, public menuService: MenuService,
      public changeDetectorRef: ChangeDetectorRef, public zone: NgZone,
      private sanitizer: DomSanitizer) {

      this.menuSourceSubscription = this.menuService.menuSource$.subscribe(key =&gt; {
        this.openNewTab(key)
      });
    }

    ngAfterViewInit(): void {
      this.closable = true
    }


    handleClose(e) {
      console.log(e.index)
      this.tabs.splice(e.index-1,1)
      if (this.tabs.length == 0){
        this.activeIndex = 0
      }else{
        if(this.activeIndex === e.index){
          this.activeIndex = e.index - 1
        }
      }
      e.close();
    }


    openNewTab(item): void {
      if (this.tabs.indexOf(item) &gt; -1) {
        setTimeout(() =&gt; {
          this.activeIndex = this.tabs.indexOf(item) + 1;
        })
      } else {
        item.routerLink = this.sanitizer.bypassSecurityTrustResourceUrl(window.location.protocol + '//' + window.location.host + '/#' + item.routerLink[0])
        console.log(item)
        this.tabs.push(item);
        setTimeout(() =&gt; {
          this.activeIndex = this.tabs.length;
        })
      }
    }


    // onContextMenu(event, id) {
    //   console.log(event.originalEvent.target)
    // }

    ngOnInit() {
    }
  }
  </code></pre>
  <p><img src="/upload/files/png/cb867ec97e66b9e33dbf9600d835226d.png" alt="">

markdown_text:
  ## requirement

  安装angular

  `npm install -g angular@cli@8.3.29`

  创建angular项目

  `ng new you project`

  加入PrimeNG框架支持

  `npm install primeng --save`

  `npm install primeicons --save`

  angular.json 加入style

  ```json
    "styles": [
                "src/styles.css",
                "node_modules/primeicons/primeicons.css",
                "node_modules/primeng/resources/primeng.min.css"
              ],
  ```

  ## 多语言支持

  `npm install --save @ngx-translate`

  app.module.ts 加入代码：

  ```typescript
  import { HttpClientModule, HttpClient } from '@angular/common/http';
  import { NgModule } from '@angular/core';
  import { BrowserModule } from '@angular/platform-browser';
  import { Route, RouterModule, Routes } from '@angular/router';
  import { TranslateModule, TranslateLoader } from '@ngx-translate/core';

  import { AppRoutingModule } from './app-routing.module';
  import { AppComponent } from './app.component';
  import { TranslateHttpLoader } from '@ngx-translate/http-loader';
  import { MainLayoutComponent } from './view/layout/main-layout/main-layout.component';
  import { SharedModule } from './shared/shared/shared.module';
  import { HelloWorldComponent } from './view/pages/hello-world/hello-world.component';

  export function HttpLoaderFactory(http: HttpClient) {
    return new TranslateHttpLoader(http);
  }

  @NgModule({
    declarations: [AppComponent, MainLayoutComponent, HelloWorldComponent],
    imports: [
      BrowserModule,
      AppRoutingModule,
      SharedModule,
      RouterModule,
      HttpClientModule,
      // 在这里配置
      TranslateModule.forRoot({
        loader: {
          provide: TranslateLoader,
          useFactory: HttpLoaderFactory,
          deps: [HttpClient],
        },
      }),
    ],
    providers: [],
    bootstrap: [AppComponent],
  })
  export class AppModule {}

  ```

  assets创建i18n文件夹，里面放入语言json文件。

  app.component.ts

  ```typescript
  import { Component } from '@angular/core';
  import { TranslateService } from '@ngx-translate/core';
  import { PrimeNGConfig } from 'primeng/api';

  @Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
  })
  export class AppComponent {
    constructor(
      private primengConfig: PrimeNGConfig,
      private translateService: TranslateService
    ) {}

    ngOnInit() {
      this.primengConfig.ripple = true;
      this.translateService.setDefaultLang('zh');
    }
    ngAfterViewInit() {
       console.log('lang ');
       this.translateService.use('zh');
       this.translateService.get('xxx')
        .subscribe(res => this.primengConfig.setTranslation(res));
    }
    translate(lang: string) {
      console.log(lang);
      this.translateService.use(lang);
      this.translateService
       .get('xxx')
       .subscribe(res => this.primengConfig.setTranslation(res));
    }
  }

  ```

  ## Route

  ```typescript
  import { HelloWorldComponent } from './view/pages/hello-world/hello-world.component';
  /*
   * @Author: C.J
   * @Date: 2022-04-29 23:00:55
   * @Last Modified by: C.J
   * @Last Modified time: 2022-04-30 00:16:46
   */
  import { NgModule } from '@angular/core';
  import { RouterModule, Routes } from '@angular/router';
  import { AppMainComponent } from './app.main.component';

  const routes: Routes = [
    {
      path: '', component: AppMainComponent,
      children: [
        { path: '', component: HelloWorldComponent }
      ]
    },
  ]

  @NgModule({
    imports: [RouterModule.forRoot(routes,  {scrollPositionRestoration: 'enabled', anchorScrolling:'enabled'}
    )],
    exports: [RouterModule]
  })
  export class AppRoutingModule { }

  ```
  ## 后台界面

  ![](/upload/files/png/03c316f11a756ba353e5ca078740fe07.png)


  调了一天CSS，MLGB。

  ## Tabview标签页

  ```typescript
  export class AppTabViewComponent implements AfterViewInit, OnInit {

    closable: boolean = true

    model: any[];

    activeIndex = 0

    @Input()
    tabs: MenuItem[] = [];

    menuSourceSubscription: Subscription;


    constructor(public appMain: AppMainComponent, public menuService: MenuService,
      public changeDetectorRef: ChangeDetectorRef, public zone: NgZone,
      private sanitizer: DomSanitizer) {

      this.menuSourceSubscription = this.menuService.menuSource$.subscribe(key => {
        this.openNewTab(key)
      });
    }

    ngAfterViewInit(): void {
      this.closable = true
    }


    handleClose(e) {
      console.log(e.index)
      this.tabs.splice(e.index-1,1)
      if (this.tabs.length == 0){
        this.activeIndex = 0
      }else{
        if(this.activeIndex === e.index){
          this.activeIndex = e.index - 1
        }
      }
      e.close();
    }


    openNewTab(item): void {
      if (this.tabs.indexOf(item) > -1) {
        setTimeout(() => {
          this.activeIndex = this.tabs.indexOf(item) + 1;
        })
      } else {
        item.routerLink = this.sanitizer.bypassSecurityTrustResourceUrl(window.location.protocol + '//' + window.location.host + '/#' + item.routerLink[0])
        console.log(item)
        this.tabs.push(item);
        setTimeout(() => {
          this.activeIndex = this.tabs.length;
        })
      }
    }


    // onContextMenu(event, id) {
    //   console.log(event.originalEvent.target)
    // }

    ngOnInit() {
    }
  }
  ```

  ![](/upload/files/png/cb867ec97e66b9e33dbf9600d835226d.png)


title: Gateway接口转发维护
created_time: 2022-05-18 06:08:09.406457
intro:
  api dispatch   在开发BI系统的时候，出现了这样一个不方便的地方。  BI报表所展示的数据是从WMS系统通过API获取的，而BI系统也有自己的后台实现了用户权限之类的。  那么现在如果WMS开发了一个新的接口，我就要在BI系统手动写一个接口，实在很不方便。解决的方法也很简单，BI系统开发一个API管理的功能，将WMS接口手动维护进去，包括URL Method Params 等。

html_text:
  <h1 id="h1-u63A5u53E3u8F6Cu53D1u8C03u7528"><a name="接口转发调用" class="reference-link"></a><span class="header-link octicon octicon-link"></span>接口转发调用</h1><h2 id="h2-u95EEu9898u63CFu8FF0"><a name="问题描述" class="reference-link"></a><span class="header-link octicon octicon-link"></span>问题描述</h2><p>在开发BI系统的时候，出现了这样一个不方便的地方。</p>
  <p>BI报表所展示的数据是从WMS系统通过API获取的，而BI系统也有自己的后台实现了用户权限之类的。</p>
  <p>那么现在如果WMS开发了一个新的接口，我就要在BI系统手动写一个接口，实在很不方便。解决的方法也很简单，BI系统开发一个API管理的功能，将WMS接口手动维护进去，包括URL Method Params 等。</p>
  <p><img src="/upload/files/png/997635b6330303e7ec78134f56f86ccb.png" alt="">
  <p>BI系统提供一个统一的WMS接口入口，我叫gateway。</p>
  <p><img src="/upload/files/png/143c0fce20054c3ea5fcbe6738207d24.png" alt="">
  <p>前端只请求gateway接口，带上接口代码和公司代码。</p>
  <h2 id="h2-vue-gateway-"><a name="Vue Gateway 代码" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Vue Gateway 代码</h2><pre><code class="lang-javascript">const api = {
    gateway (api, params) {
      const formData = new FormData()
      formData.append('params', JSON.stringify(params))
      return new Promise((resolve, reject) =&gt; {
        request({
          url: '/gateway/dispatch/' + store.getters.getCompanyCode + '/' + store.getters.getCompanyCode + '-' + api,
          method: 'post',
          data: formData
        })
          .then((resp) =&gt; {
            // eslint-disable-next-line no-prototype-builtins
            if (resp.data.hasOwnProperty('ErrorMessages')) {
              window.CIEMIS.$emit('SERVER_ERROR', {
                text: resp.data.ErrorMessages[0]
              })
            }
            resolve(resp.data)
          })
          .catch((err) =&gt; {
            // eslint-disable-next-line no-prototype-builtins
            reject(err)
          })
      })
    }
  }
  </code></pre>
  <p>BI不止为一个公司服务，因此公司代码是动态获取的，不同的公司可以动态切换。</p>
  <h2 id="h2-bi-"><a name="BI 后台实现" class="reference-link"></a><span class="header-link octicon octicon-link"></span>BI 后台实现</h2><pre><code class="lang-java">    @RequestMapping("/dispatch/{companyCode}/{api}")
      @ResponseBody
      public ResponseData gateway(@PathVariable String companyCode, @PathVariable String api, String params) {
          Company company = companyRepository.findCompanyByCode(companyCode);
          Gateway gateway = gatewayRepository.findGatewayByCode(api);
          if (Objects.isNull(company)) {
              throw new NullPointerException("公司异常");
          }
          if (Objects.isNull(gateway)) {
              throw new NullPointerException("没有维护API");
          }

          String url = company.getWebAddress() + gateway.getUrl();
          Map&lt;String, String&gt; paramsMap = new HashMap&lt;&gt;();
          // 验证接口参数一致性
          JSONArray array = JSONArray.parseArray(gateway.getParams());
          JSONObject paramsObj = JSONObject.parseObject(params);
          for (int i = 0; i &lt; array.size(); i++) {
              String paramName = array.getJSONObject(i).getString("name");
              if (Objects.isNull(paramsObj.getString(paramName))) {
                  throw new ServiceException(500, "gateway参数校验失败");
              }
              paramsMap.put(paramName, paramsObj.getString(paramName));
          }
          String result;
          try {
              if (gateway.getMethod().equals("POST")) {
                  result = OkHttpUtil.sendPostRequest(url, paramsMap).body().string();
              } else {
                  result = OkHttpUtil.get(url, paramsMap).body().string();
              }
          } catch (IOException e) {
              throw new ServiceException(500, "gateway IO Exception: " + e.getMessage());
          }
          Object o = JSON.parse(result);
          if (o instanceof JSONArray) {
              return ResponseData.success(200, "OK", JSON.parseArray(result));
          } else {
              return ResponseData.success(200, "OK", JSON.parseObject(result));
          }
      }
  </code></pre>
  <h2 id="h2-u5B9Eu9645u4F7Fu7528"><a name="实际使用" class="reference-link"></a><span class="header-link octicon octicon-link"></span>实际使用</h2><pre><code class="lang-javascript">api.gateway('BIScrapActions', {
    PlantCode: 'P01',
    RptType: 'Scrap'
  }).then((resp) =&gt; {
    this.data = resp
    this.initCharts()
  })
  </code></pre>
  <p>这样就完成了。</p>
  <h3 id="h3-postman-"><a name="POSTMAN 参数自动格式化" class="reference-link"></a><span class="header-link octicon octicon-link"></span>POSTMAN 参数自动格式化</h3><pre><code class="lang-javascript">  this.params_table = val.split(' ').map((item) =&gt; {
      return {
        name: item.split(':')[0],
        value: item.split(':')[1]
      }
    })
  },
  </code></pre>
  <h2 id="h2-u63D0u4F9Bu63A5u53E3u6D4Bu8BD5"><a name="提供接口测试" class="reference-link"></a><span class="header-link octicon octicon-link"></span>提供接口测试</h2><p>在接口维护的时候会填写参数KV，值为测试值，可以在网页测试接口是否通过。在实际调用中会对参数进行匹配。</p>
  <p><img src="/upload/files/png/f850f500e07fca398180c3dfd38bd513.png" alt="">

markdown_text:
  # 接口转发调用

  ## 问题描述

  在开发BI系统的时候，出现了这样一个不方便的地方。

  BI报表所展示的数据是从WMS系统通过API获取的，而BI系统也有自己的后台实现了用户权限之类的。

  那么现在如果WMS开发了一个新的接口，我就要在BI系统手动写一个接口，实在很不方便。解决的方法也很简单，BI系统开发一个API管理的功能，将WMS接口手动维护进去，包括URL Method Params 等。

  ![](/upload/files/png/997635b6330303e7ec78134f56f86ccb.png)


  BI系统提供一个统一的WMS接口入口，我叫gateway。

  ![](/upload/files/png/143c0fce20054c3ea5fcbe6738207d24.png)

  前端只请求gateway接口，带上接口代码和公司代码。

  ## Vue Gateway 代码

  ```javascript
  const api = {
    gateway (api, params) {
      const formData = new FormData()
      formData.append('params', JSON.stringify(params))
      return new Promise((resolve, reject) => {
        request({
          url: '/gateway/dispatch/' + store.getters.getCompanyCode + '/' + store.getters.getCompanyCode + '-' + api,
          method: 'post',
          data: formData
        })
          .then((resp) => {
            // eslint-disable-next-line no-prototype-builtins
            if (resp.data.hasOwnProperty('ErrorMessages')) {
              window.CIEMIS.$emit('SERVER_ERROR', {
                text: resp.data.ErrorMessages[0]
              })
            }
            resolve(resp.data)
          })
          .catch((err) => {
            // eslint-disable-next-line no-prototype-builtins
            reject(err)
          })
      })
    }
  }
  ```

  BI不止为一个公司服务，因此公司代码是动态获取的，不同的公司可以动态切换。



  ## BI 后台实现

  ```java
      @RequestMapping("/dispatch/{companyCode}/{api}")
      @ResponseBody
      public ResponseData gateway(@PathVariable String companyCode, @PathVariable String api, String params) {
          Company company = companyRepository.findCompanyByCode(companyCode);
          Gateway gateway = gatewayRepository.findGatewayByCode(api);
          if (Objects.isNull(company)) {
              throw new NullPointerException("公司异常");
          }
          if (Objects.isNull(gateway)) {
              throw new NullPointerException("没有维护API");
          }

          String url = company.getWebAddress() + gateway.getUrl();
          Map<String, String> paramsMap = new HashMap<>();
          // 验证接口参数一致性
          JSONArray array = JSONArray.parseArray(gateway.getParams());
          JSONObject paramsObj = JSONObject.parseObject(params);
          for (int i = 0; i < array.size(); i++) {
              String paramName = array.getJSONObject(i).getString("name");
              if (Objects.isNull(paramsObj.getString(paramName))) {
                  throw new ServiceException(500, "gateway参数校验失败");
              }
              paramsMap.put(paramName, paramsObj.getString(paramName));
          }
          String result;
          try {
              if (gateway.getMethod().equals("POST")) {
                  result = OkHttpUtil.sendPostRequest(url, paramsMap).body().string();
              } else {
                  result = OkHttpUtil.get(url, paramsMap).body().string();
              }
          } catch (IOException e) {
              throw new ServiceException(500, "gateway IO Exception: " + e.getMessage());
          }
          Object o = JSON.parse(result);
          if (o instanceof JSONArray) {
              return ResponseData.success(200, "OK", JSON.parseArray(result));
          } else {
              return ResponseData.success(200, "OK", JSON.parseObject(result));
          }
      }


  ```



  ## 实际使用

  ```javascript
  api.gateway('BIScrapActions', {
    PlantCode: 'P01',
    RptType: 'Scrap'
  }).then((resp) => {
    this.data = resp
    this.initCharts()
  })
  ```

  这样就完成了。



  ### POSTMAN 参数自动格式化

  ```javascript
    this.params_table = val.split(' ').map((item) => {
      return {
        name: item.split(':')[0],
        value: item.split(':')[1]
      }
    })
  },
  ```



  ## 提供接口测试

  在接口维护的时候会填写参数KV，值为测试值，可以在网页测试接口是否通过。在实际调用中会对参数进行匹配。

  ![](/upload/files/png/f850f500e07fca398180c3dfd38bd513.png)


title: 控制回路中的DI，DO，AI，AO信号有什么用途？什么意思？
created_time: 2022-06-10 06:34:39.579843
intro:
  1.DI信号：数字输入信号（digital input）  所谓数字信号，及高电平1和低电平0，在控制上也就是断开或闭合，DI就是一个开关量的输入信号，给PLC或DCS一个断开和闭合的命令，和常开启动按钮开关一样，按下就是一个闭合信号；常用于状态的显示，比如断路器合闸了，在DCS画面显示闭合。

html_text:
  <h3>1.DI信号：数字输入信号（digital input）</h3><p>所谓数字信号，及高电平1和低电平0，在控制上也就是断开或闭合，DI就是一个开关量的输入信号，给PLC或DCS一个断开和闭合的命令，和常开启动按钮开关一样，按下就是一个闭合信号；常用于状态的显示，比如断路器合闸了，在DCS画面显示闭合。</p><h3>2.DO信号：数字输出信号（digital output）</h3><p>是一种开关量的输出信号，即PLC发出一个启动或停止的信号给设备，使设备启动；比如电机的启动，通过DCS认为发出启动命令，电机控制回路接通，接触器得电吸合，电机启动；功能和启动按钮一样。</p><h3>3.AI信号：模拟量输入</h3><p>所谓模拟量即4-20mA或0-10V的电流或电压信号，输入到DCS中，经过变换，还原出原来的值，比如电流值到DCS中显示出来；电流互感器二次值（5A）经过电流变送器转换成4-20mA信号（5A对应20mA）,输入到DCS机柜，经过转换，在DCS中显示实际的电流值。</p><h3>4.AO信号：模拟量输出</h3><p>和AI相反，AO是DCS输出4-20mA或0-10V信号，去控制设备的运行参数；比如，变频器的速度控制过程，以40HZ为例，通过远程DCS控制，认为给定参数值：40HZ，DCS同样发出一个对应的模拟量电流值，在4-20mA范围内（20mA对应50HZ），信号传入到变频器的控制器，变频器按照要求运行在40HZ。</p><p><br></p>

markdown_text:
  1.DI信号：数字输入信号（digital input）
  所谓数字信号，及高电平1和低电平0，在控制上也就是断开或闭合，DI就是一个开关量的输入信号，给PLC或DCS一个断开和闭合的命令，和常开启动按钮开关一样，按下就是一个闭合信号；常用于状态的显示，比如断路器合闸了，在DCS画面显示闭合。
  2.DO信号：数字输出信号（digital output）
  是一种开关量的输出信号，即PLC发出一个启动或停止的信号给设备，使设备启动；比如电机的启动，通过DCS认为发出启动命令，电机控制回路接通，接触器得电吸合，电机启动；功能和启动按钮一样。
  3.AI信号：模拟量输入
  所谓模拟量即4-20mA或0-10V的电流或电压信号，输入到DCS中，经过变换，还原出原来的值，比如电流值到DCS中显示出来；电流互感器二次值（5A）经过电流变送器转换成4-20mA信号（5A对应20mA）,输入到DCS机柜，经过转换，在DCS中显示实际的电流值。
  4.AO信号：模拟量输出
  和AI相反，AO是DCS输出4-20mA或0-10V信号，去控制设备的运行参数；比如，变频器的速度控制过程，以40HZ为例，通过远程DCS控制，认为给定参数值：40HZ，DCS同样发出一个对应的模拟量电流值，在4-20mA范围内（20mA对应50HZ），信号传入到变频器的控制器，变频器按照要求运行在40HZ。


title: AliOS Things HaaS510 开发（1）
intro: AliOS Things HaaS510 开发
created_time: 2022-06-15 05:06:57.964618
html_text:
  <h2 id="h2-requirement"><a name="requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>requirement</h2><p><a href="https://hli.aliyuncs.com/o/config/firmware/amp-clitool-haas510.zip">Amp 烧录工具</a></p>
  <p><a href="https://s3.z100.vip:28592/CJ/%E7%89%A9%E8%81%94%E7%BD%91/SSCOM32.rar">串口调试工具</a></p>
  <p>USB转TTL串口模块</p>
  <p><img src="https://yangchaojie.cn:9816/upload/files/png/c4fd8a169d332f2eb590c1e99effa789.png" alt="image-20220615101543435" style="zoom: 25%;">
  <h2 id="h2-u5F15u811Au63A5u7EBF"><a name="引脚接线" class="reference-link"></a><span class="header-link octicon octicon-link"></span>引脚接线</h2><h4 id="h4-main-uart-to-usb"><a name="MAIN UART TO USB" class="reference-link"></a><span class="header-link octicon octicon-link"></span>MAIN UART TO USB</h4><p>GND -&gt; GND</p>
  <p>RX2 -&gt; TXD</p>
  <p>TX2 -&gt; RXD</p>
  <p>5V -&gt; 5V</p>
  <h4 id="h4-debug-uart-to-usb"><a name="DEBUG UART TO USB" class="reference-link"></a><span class="header-link octicon octicon-link"></span>DEBUG UART TO USB</h4><p>RX2 -&gt; TXD</p>
  <p>TX2 -&gt; RXD</p>
  <h2 id="h2-u70E7u5F55u6D4Bu8BD5"><a name="烧录测试" class="reference-link"></a><span class="header-link octicon octicon-link"></span>烧录测试</h2><pre><code class="lang-bash">E:\alios things\amp-clitool\dist\amp-win&gt;amp.exe serialput "E:\alios things\amp-clitool\dist\amp-win\app\solutions\test_demo" COM8
  device type: ymodem
  generating app package...
  Browserslist: caniuse-lite is outdated. Please run:
  npx browserslist@latest --update-db

  Why you should do it regularly:
  https://github.com/browserslist/browserslist#browsers-data-updating
  E:\alios things\amp-clitool\dist\amp-win\app.bin
  Wait shakehand with board ...
  Please reboot board now
  number
  { current: 0, total: 404 }
  { current: 128, total: 404 }
  { current: 256, total: 404 }
  { current: 384, total: 404 }
  { current: 20, total: 404 }
  put complete!
  </code></pre>
  <h2 id="h2--debug-"><a name="查看DEBUG日志" class="reference-link"></a><span class="header-link octicon octicon-link"></span>查看DEBUG日志</h2><p><img src="https://yangchaojie.cn:9816/upload/files/png/b417e337450b629d2d7eeebe3e7c9bb1.png" alt="image-20220615101543435">
  <p>HelloWorld案例：</p>
  <pre><code class="lang-js">console.log('hello amp!');

  setInterval(function () {

    console.log('Hello World, time: ' + new Date());

  }, 1000);
  </code></pre>
  <p><img src="https://yangchaojie.cn:9816/upload/files/jpg/f793a7fb1e12d76b2eb877f592020bee.jpg" alt="image-20220615101543435" style="zoom: 50%;">
  <p>目前还没有SIM卡和相关配件，先到此为止。</p>

markdown_text:
  ## requirement

  [Amp 烧录工具](https://hli.aliyuncs.com/o/config/firmware/amp-clitool-haas510.zip)

  [串口调试工具](https://s3.z100.vip:28592/CJ/%E7%89%A9%E8%81%94%E7%BD%91/SSCOM32.rar)

  USB转TTL串口模块

  <img src="https://yangchaojie.cn:9816/upload/files/png/c4fd8a169d332f2eb590c1e99effa789.png" alt="image-20220615101543435" style="zoom: 25%;" />

  ## 引脚接线

  #### MAIN UART TO USB 

  GND -> GND

  RX2 -> TXD

  TX2 -> RXD

  5V -> 5V

  #### DEBUG UART TO USB

  RX2 -> TXD

  TX2 -> RXD

  ## 烧录测试

  ```bash
  E:\alios things\amp-clitool\dist\amp-win>amp.exe serialput "E:\alios things\amp-clitool\dist\amp-win\app\solutions\test_demo" COM8
  device type: ymodem
  generating app package...
  Browserslist: caniuse-lite is outdated. Please run:
  npx browserslist@latest --update-db

  Why you should do it regularly:
  https://github.com/browserslist/browserslist#browsers-data-updating
  E:\alios things\amp-clitool\dist\amp-win\app.bin
  Wait shakehand with board ...
  Please reboot board now
  number
  { current: 0, total: 404 }
  { current: 128, total: 404 }
  { current: 256, total: 404 }
  { current: 384, total: 404 }
  { current: 20, total: 404 }
  put complete!
  ```

  ## 查看DEBUG日志

  <img src="https://yangchaojie.cn:9816/upload/files/png/b417e337450b629d2d7eeebe3e7c9bb1.png" alt="image-20220615101543435" />


  HelloWorld案例：

  ```js
  console.log('hello amp!');

  setInterval(function () {

    console.log('Hello World, time: ' + new Date());

  }, 1000);

  ```
  <img src="https://yangchaojie.cn:9816/upload/files/jpg/f793a7fb1e12d76b2eb877f592020bee.jpg" alt="image-20220615101543435" style="zoom: 50%;" />

  目前还没有SIM卡和相关配件，先到此为止。


title: HaaS 531 烧录、控制继电器
intro: HaaS 531 烧录、控制继电器
created_time: 2022-07-06 01:41:11.218542
html_text:
  <h1 id="h1-haas-531-"><a name="HaaS 531 烧录、控制继电器" class="reference-link"></a><span class="header-link octicon octicon-link"></span>HaaS 531 烧录、控制继电器</h1><h2 id="h2-requirement"><a name="requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>requirement</h2><p><img src="/upload/files/png/692cabfa6a47e4e2bcb33140d93c4730.png" alt="image-20220706092227096" style="zoom: 25%;width:50%">
  <p><strong>Micro USB</strong> 数据线</p>
  <p>USB转485接口</p>
  <p>继电器连接至531 COM1口</p>
  <h2 id="h2--log"><a name="烧录程序-查看LOG" class="reference-link"></a><span class="header-link octicon octicon-link"></span>烧录程序-查看LOG</h2><p>COM5为531端口，通过此端口烧录JS程序</p>
  <p>Please reboot board now 时断电</p>
  <p><code>E:\alios things\amp-win&gt;amp.exe serialput ./app COM5 -b 9600
  generating app package...
  Wait shakehand with board ...
  Please reboot board now
  Putting: ████████░░░░░░░░░░░░░░░░░░░░░░ 2/7</code></p>
  <p>-b 9600为必须。</p>
  <p>烧录完成进入USB 15口查看LOG：</p>
  <p><img src="/upload/files/png/876d369bcdeb6bc19feab5ac9cdd67eb.png" alt="">
  <p>程序通过COM1口向继电器发送指令循环控制开关。</p>
  <pre><code class="lang-js">setInterval(() =&gt; {
      let openAll = new Uint8Array([0x48, 0x3a, 0x01, 0x57, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xde, 0x45, 0x44])
      com2.write(openAll);
      setTimeout(()=&gt;{
          let closeAll = new Uint8Array([0x48, 0x3a, 0x01, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xda, 0x45, 0x44])
          com2.write(closeAll);
      },1000)
      console.log("send complate")
  }, 2000)
  </code></pre>
  <p>发送数据代码如上，必须是ArrayBuff类型的16进制数据。</p>
  <h2 id="h2-u8BFBu503C"><a name="读值" class="reference-link"></a><span class="header-link octicon octicon-link"></span>读值</h2><pre><code class="lang-js">/* com2 receive data */
  com1.on('data', function (data) {
     console.log(buf2hex(data))
  });
  </code></pre>
  <pre><code class="lang-js">function buf2hex(buffer) { // buffer is an ArrayBuffer
      return [...new Uint8Array(buffer)]
          .map(x =&gt; x.toString(16).padStart(2, '0'))
          .join('');
    }
  </code></pre>
  <p>继电器返回数据也必须用ArrayBuffer接收。</p>

markdown_text:
  # HaaS 531 烧录、控制继电器

  ## requirement
  <img src="/upload/files/png/692cabfa6a47e4e2bcb33140d93c4730.png" alt="image-20220706092227096" style="zoom: 25%;width:50%" />

  **Micro USB** 数据线

  USB转485接口

  继电器连接至531 COM1口

  ## 烧录程序-查看LOG

  COM5为531端口，通过此端口烧录JS程序

  Please reboot board now 时断电

  `E:\alios things\amp-win>amp.exe serialput ./app COM5 -b 9600
  generating app package...
  Wait shakehand with board ...
  Please reboot board now
  Putting: ████████░░░░░░░░░░░░░░░░░░░░░░ 2/7`

  -b 9600为必须。

  烧录完成进入USB 15口查看LOG：


  ![](/upload/files/png/876d369bcdeb6bc19feab5ac9cdd67eb.png)

  程序通过COM1口向继电器发送指令循环控制开关。

  ```js
  setInterval(() => {
      let openAll = new Uint8Array([0x48, 0x3a, 0x01, 0x57, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xde, 0x45, 0x44])
      com2.write(openAll);
      setTimeout(()=>{
          let closeAll = new Uint8Array([0x48, 0x3a, 0x01, 0x57, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xda, 0x45, 0x44])
          com2.write(closeAll);
      },1000)
      console.log("send complate")
  }, 2000)
  ```

  发送数据代码如上，必须是ArrayBuff类型的16进制数据。

  ## 读值

  ```js
  /* com2 receive data */
  com1.on('data', function (data) {
     console.log(buf2hex(data))
  });

  ```

  ```js
  function buf2hex(buffer) { // buffer is an ArrayBuffer
      return [...new Uint8Array(buffer)]
          .map(x => x.toString(16).padStart(2, '0'))
          .join('');
    }
    
  ```

  继电器返回数据也必须用ArrayBuffer接收。


title: Incremental data ETL
intro: Incremental data ETL
created_time: 2022-08-30 08:10:40.677986
html_text:
  <p>MongoDb增量数据读取，最后一条数据根据 ObjectId + ISODate 作为 标记，下次读取 &gt; ObjectId &gt;= ISODate 即可继续读取。<br><img src="/upload/files/png/ccb0ec18b42272371fb002ca0fc45df5.png" alt="">
  <pre><code class="lang-python">import datetime
  import os
  import dateutil.parser
  from bson import ObjectId

  import LoggerLocal as log
  import DBCollection

  name = os.path.basename(__file__)
  # 120,MesTestDetail.py,"",2022-08-30 14:30:35,mes_mongodb,MesTestDetail,Angle45°_AngleCheck

  filter_item = ['Angle45°_AngleCheck']

  MAX_HANDLE_LINE = 100

  mssql_db = DBCollection.get_dbobj("mes_mssql", "mssql")
  mongodb = DBCollection.get_dbobj("mes_mongodb", "mongodb")


  def MesTestDetail():
      global mssql_db
      global mongodb
      try:
          log.info(f"======={datetime.datetime.now()} {name} 执行=================")
          mssql_cursor = mssql_db.cursor(as_dict=True)
          for item in filter_item:
              point = DBCollection.get_point(name, 'mes_mongodb', 'MesTestDetail', item)
              client = mongodb['hasco_mes']
              tb = client['MES_MesTestDetail']
              if point[0][2] == '':
                  myquery = {"TestItem": item, "ItemNo": {"$exists": True}}
              else:
                  id_str = str(point[0][2]).split("#")[0]
                  log_date = str(point[0][2]).split("#")[1]
                  obj_id = ObjectId(id_str)
                  date = dateutil.parser.isoparse(log_date)
                  myquery = {"TestItem": item, "ItemNo": {"$exists": True}, "LogDate": {"$gte": date},
                             "_id": {'$gt': obj_id}}
              cols = tb.find(myquery).sort("LogDate").limit(MAX_HANDLE_LINE)
              point_obj_id = ''
              point_log_date = ''
              for data in cols:
                  print(data)
                  mssql_cursor.executemany(
                      "INSERT INTO dbo.Mes_XRTestDetail VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
                      [(data["Barcode"], data["ItemNo"], data['StartTime'], data['EndTime'], data['TestItem'],
                        data['Value01'], data['Value02'], data['Value03'], data['Value04'], data['Value05'],
                        data['LogDate'], data['LogUser'])])
                  point_obj_id = data['_id']
                  point_log_date = data['LogDate']
              log.info(f"item:{item} insert success!")
              log.info( f"{point_obj_id}#{point_log_date}")
              DBCollection.set_point(name, 'mes_mongodb', 'MesTestDetail', f"{point_obj_id}#{point_log_date}", item_key=item)
              mssql_db.commit()

              log.info("执行完成。")
              DBCollection.set_result(name, True, log.get_buffer())
      except Exception as e:
          mssql_db.rollback()
          DBCollection.job_db.rollback()
          log.info(f"事务已回滚")
          log.info(f"执行[{name}]失败 异常:{e}")
          DBCollection.set_result(script=name, result=False, log_text=log.get_buffer())
          print(f"执行[{name}]失败,  异常:{e}")


  if __name__ == '__main__':
      MesTestDetail()
  </code></pre>

markdown_text:
  MongoDb增量数据读取，最后一条数据根据 ObjectId + ISODate 作为 标记，下次读取 > ObjectId >= ISODate 即可继续读取。
  ![](/upload/files/png/ccb0ec18b42272371fb002ca0fc45df5.png)
  ```python
  import datetime
  import os
  import dateutil.parser
  from bson import ObjectId

  import LoggerLocal as log
  import DBCollection

  name = os.path.basename(__file__)
  # 120,MesTestDetail.py,"",2022-08-30 14:30:35,mes_mongodb,MesTestDetail,Angle45°_AngleCheck

  filter_item = ['Angle45°_AngleCheck']

  MAX_HANDLE_LINE = 100

  mssql_db = DBCollection.get_dbobj("mes_mssql", "mssql")
  mongodb = DBCollection.get_dbobj("mes_mongodb", "mongodb")


  def MesTestDetail():
      global mssql_db
      global mongodb
      try:
          log.info(f"======={datetime.datetime.now()} {name} 执行=================")
          mssql_cursor = mssql_db.cursor(as_dict=True)
          for item in filter_item:
              point = DBCollection.get_point(name, 'mes_mongodb', 'MesTestDetail', item)
              client = mongodb['hasco_mes']
              tb = client['MES_MesTestDetail']
              if point[0][2] == '':
                  myquery = {"TestItem": item, "ItemNo": {"$exists": True}}
              else:
                  id_str = str(point[0][2]).split("#")[0]
                  log_date = str(point[0][2]).split("#")[1]
                  obj_id = ObjectId(id_str)
                  date = dateutil.parser.isoparse(log_date)
                  myquery = {"TestItem": item, "ItemNo": {"$exists": True}, "LogDate": {"$gte": date},
                             "_id": {'$gt': obj_id}}
              cols = tb.find(myquery).sort("LogDate").limit(MAX_HANDLE_LINE)
              point_obj_id = ''
              point_log_date = ''
              for data in cols:
                  print(data)
                  mssql_cursor.executemany(
                      "INSERT INTO dbo.Mes_XRTestDetail VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",
                      [(data["Barcode"], data["ItemNo"], data['StartTime'], data['EndTime'], data['TestItem'],
                        data['Value01'], data['Value02'], data['Value03'], data['Value04'], data['Value05'],
                        data['LogDate'], data['LogUser'])])
                  point_obj_id = data['_id']
                  point_log_date = data['LogDate']
              log.info(f"item:{item} insert success!")
              log.info( f"{point_obj_id}#{point_log_date}")
              DBCollection.set_point(name, 'mes_mongodb', 'MesTestDetail', f"{point_obj_id}#{point_log_date}", item_key=item)
              mssql_db.commit()

              log.info("执行完成。")
              DBCollection.set_result(name, True, log.get_buffer())
      except Exception as e:
          mssql_db.rollback()
          DBCollection.job_db.rollback()
          log.info(f"事务已回滚")
          log.info(f"执行[{name}]失败 异常:{e}")
          DBCollection.set_result(script=name, result=False, log_text=log.get_buffer())
          print(f"执行[{name}]失败,  异常:{e}")


  if __name__ == '__main__':
      MesTestDetail()

  ```


title: MI OPC协议及MQTT
created_time: 2022-09-06 03:29:03.428065
intro:
  使用OPC协议读取注塑机设备的值。  经过考察，当前情况是注塑机没有加入到KEPWARE ，无法通过TCP访问当设备数据。  所以目前只能在机器本地开发，选用Python开发，使用 OpenOpc 库。  OpenOpc:  http://openopc.sourceforge.net/  MQTT 使用 EMQX作为MQ平台：https://www.emqx.io/zh/downloads?os=CentOS  目前将注塑机数据上传值MQTT，内网环境。

html_text:
  <h1 id="h1-opc-mqtt"><a name="OPC协议及MQTT" class="reference-link"></a><span class="header-link octicon octicon-link"></span>OPC协议及MQTT</h1><h2 id="h2-requirement"><a name="Requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Requirement</h2><p>使用OPC协议读取注塑机设备的值。</p>
  <p>经过考察，当前情况是注塑机没有加入到KEPWARE ，无法通过TCP访问当设备数据。</p>
  <p>所以目前只能在机器本地开发，选用Python开发，使用 OpenOpc 库。</p>
  <p>OpenOpc:  <a href="http://openopc.sourceforge.net/">http://openopc.sourceforge.net/</a></p>
  <p>MQTT 使用 EMQX作为MQ平台：<a href="https://www.emqx.io/zh/downloads?os=CentOS">https://www.emqx.io/zh/downloads?os=CentOS</a></p>
  <p>目前将注塑机数据上传值MQTT，内网环境。</p>
  <h2 id="h2-u53D6u503C"><a name="取值" class="reference-link"></a><span class="header-link octicon octicon-link"></span>取值</h2><p>OpenOpc 的read方法有点问题，会出现读值慢，无法读下一个值的情况，我猜测这是单线程订阅。使用properties方法可以解决。</p>
  <p>代码(初版 已废弃)：</p>
  <pre><code class="lang-python"># ZPF
  # encoding=utf-8
  import datetime
  import json
  import sys
  import time
  import traceback
  from configparser import ConfigParser

  import OpenOPC
  import pywintypes
  import servicemanager
  import win32serviceutil
  import win32service
  import win32event
  import os
  import logging
  import inspect
  from paho.mqtt import client as mqtt_client
  from pymongo import MongoClient
  import win32timezone
  import dateutil.parser

  broker = '172.xxx'
  port = 1883
  topic = ""
  # generate client ID with pub prefix randomly
  client_id = 'xx'
  user = 'xx'
  pwd = 'xx'

  topic_list = ["tmMachID", "tmOperateMode", "tmAlarmState", "tmAlarmID", "tmCraftID", "tmCraftID", "tmCycleTime",
                "tmPlanCount", "tmShotCount", "tmInjHighPress", "tmInjStartPosi", "tmCycleTime", "tmInjTime",
                "tmClpClsTime", "tmClpOpnTime", "tmClpOpnPosi", "tmInjEndPosi", "tmTemp1_Set", "tmTemp2_Set",
                "tmTemp3_Set", "tmTemp4_Set", "tmTemp5_Set", "tmTemp6_Set", "tmTemp1_Current", "tmTemp2_Current",
                "tmTemp3_Current", "tmTemp4_Current", "tmTemp5_Current", "tmTemp6_Current", "tmTempOil_Current"]

  BUFFER = []


  def connect_mqtt(broker, port, client_id, user, pwd):
      def on_connect(client, userdata, flags, rc):
          if rc == 0:
              print("Connected to MQTT Broker!")
          else:
              print("Failed to connect, return code %d\n", rc)

      client = mqtt_client.Client(client_id=client_id, clean_session=False)
      client.username_pw_set(user, pwd)
      client.on_connect = on_connect
      client.connect(broker, port)
      return client


  class Config(object):
      def __init__(self):
          self.conf = ConfigParser()
          self.path = os.path.expanduser('~') + '\\' + '.ciemis_mi'
          self.name = "setting.ini"
          self.dir = self.path + '\\' + self.name
          if not os.path.isdir(self.path):
              print(self.path)
              print("创建...")
              os.makedirs(self.path)
          if not os.path.isfile(self.dir):
              fd = open(self.dir, mode="w", encoding="utf-8")
              fd.close()
              self.init_data()

      # 初始化数据
      def init_data(self):
          self.conf.add_section("TOPIC")
          self.conf.set("TOPIC", "BROKER", broker)
          self.conf.set("TOPIC", "PORT", str(port))
          self.conf.set("TOPIC", "CLIENT_ID", client_id)
          self.conf.set("TOPIC", "URL", topic)
          self.conf.set("TOPIC", "USERNAME", user)
          self.conf.set("TOPIC", "PASSWORD", pwd)
          self.conf.set("TOPIC", "TOPIC_LIST", json.dumps(topic_list))

          self.conf.add_section("MONGODB")
          self.conf.set("MONGODB", "host",
                        "xx")
          self.conf.set("MONGODB", "port", str(3717))
          self.conf.set("MONGODB", "username", "xx")
          self.conf.set("MONGODB", "password", "xx")
          self.conf.set("MONGODB", "db", "xx")
          self.conf.set("MONGODB", "col", "xx")

          self.conf.write(open(self.dir, 'w'))

      def get_topic(self, key: str, load: bool = False):
          self.conf.read(self.dir)
          data = self.conf.get('TOPIC', key)
          if load:
              data = json.loads(data)
          return data

      def get_mongo(self, key: str, load: bool = False):
          self.conf.read(self.dir)
          data = self.conf.get('MONGODB', key)
          if load:
              data = json.loads(data)
          return data


  class OpcService(win32serviceutil.ServiceFramework):
      _svc_name_ = "CiemisOpcSrvice"
      _svc_display_name_ = "Ciemis MI Background Task"
      _svc_description_ = "云智 注塑机 MI Service"

      def __init__(self, args):
          win32serviceutil.ServiceFramework.__init__(self, args)
          self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
          self.logger = self._getLogger()
          self.run = True

      def find_value(self, topic_list, key):
          try:
              if topic_list.index(key) &gt; -1:
                  return True
          except Exception as e:
              return False

      def check_value_change(self, node):
          global BUFFER
          if len(BUFFER) == 0:
              return True
          find_res = None
          for item in BUFFER:
              if item['machine_id'] == node['machine_id'] and item['key'] == node['key']:
                  find_res = item
                  break
          if find_res is None:
              return True
          return find_res['value'] != node['value']

      def save_node(self, node):
          global BUFFER
          if len(BUFFER) == 0:
              return True
          find_index = None
          for index, value in enumerate(BUFFER):
              if value['machine_id'] == node['machine_id'] and value['key'] == node['key']:
                  find_index = index
                  break
          if find_index is None:
              BUFFER.append(node)
          else:
              del BUFFER[find_index]
              BUFFER.append(node)

      def publish(self, client, url, topic_list, col):
          pywintypes.datetime = pywintypes.TimeType
          opc = OpenOPC.client()
          open_server = 'Techmation.OPC.Server.V1'
          opc.connect(opc_server=open_server)
          self.logger.info(opc.info())
          print(self.run)
          print("start ..")
          self.logger.info("start..")
          while self.run:
              try:
                  for machine in opc.list():
                      if opc.properties(machine + ".tmAlarmDateTime")[2][2] != 0:
                          msg = {
                              "create_date": datetime.datetime.utcnow()
                          }
                          for key in opc.list(machine):

                              if self.find_value(topic_list, key):
                                  msg[key] = opc.properties(machine + "." + key, id=2)
                          # 删除 &gt; 10 min
                          now = datetime.datetime.utcnow()
                          now_cut_10 = now - datetime.timedelta(minutes=10)
                          date = dateutil.parser.isoparse(str(now_cut_10))
                          query = {"tmMachID": msg['tmMachID'], 'tmCraftID': msg['tmCraftID'],
                                   'tmShotCount': msg['tmShotCount'], 'create_date': {"$gte": date}}
                          t_url = '/' + msg['tmMachID']
                          result = client.publish(t_url, json.dumps(msg, default=str))
                          status = result[0]
                          if status == 0:
                              self.logger.info(
                                  "Send {msg} to topic {topic}".format(msg=json.dumps(msg, default=str),
                                                                       topic=t_url))
                              col.delete_many(query)
                              col.insert_one(msg)
                          else:
                              self.logger.error(
                                  "Failed to send message to topic {topic}".format(topic=t_url))
              except  Exception as e:
                  traceback.print_exc()
                  self.logger.exception(f"main exception: {str(e)}")
                  self.logger.error("TimeoutError occured")
              time.sleep(10)

      def _getLogger(self):
          logger = logging.getLogger('[CiemisOpcSrvice]')

          handler = logging.FileHandler("E:\ciemis\service.log")

          formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
          handler.setFormatter(formatter)

          logger.addHandler(handler)
          logger.setLevel(logging.INFO)

          return logger

      def SvcDoRun(self):
          self.logger.info("service is running")
          config = Config()
          self.logger.info("config check ..")
          self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
          client_id = config.get_topic("client_id")
          broker = config.get_topic("broker")
          url = config.get_topic("url")
          port = config.get_topic("port")
          user = config.get_topic("username")
          pwd = config.get_topic("password")
          self.logger.info(broker)
          self.logger.info(url)
          self.logger.info(port)
          self.logger.info(user)
          self.logger.info(pwd)
          self.logger.info(topic_list)
          self.logger.info("config check ok..")

          mongo_host = "xx"
          mongo_port = 3717
          mongo_username = 'xx'
          mongo_password = 'xx'
          mongo_db = 'xx'
          mongo_col = 'xx'
          self.logger.info(f"host:{mongo_host}")
          self.logger.info(f"mongo_db:{mongo_db}")
          self.logger.info(f"mongo_col:{mongo_col}")

          client = MongoClient(host=mongo_host, port=int(mongo_port), username=mongo_username,
                               password=mongo_password)  # 生成mongodb对象
          db = client[mongo_db]
          col = db[mongo_col]
          self.ReportServiceStatus(win32service.SERVICE_RUNNING)

          try:
              client = connect_mqtt(broker, int(port), client_id, user, pwd)
              client.loop_start()
              self.logger.info("connect ok..")
              self.publish(client, url, topic_list, col)
              win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
          except Exception as e:
              traceback.print_exc()
              self.logger.info(e)

      def SvcStop(self, message: str = "service is stop"):
          self.logger.info(message)
          self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
          win32event.SetEvent(self.hWaitStop)
          self.run = False


  if __name__ == '__main__':
      if len(sys.argv) == 1:
          servicemanager.Initialize()
          servicemanager.PrepareToHostSingle(OpcService)
          servicemanager.StartServiceCtrlDispatcher()
      else:
          win32serviceutil.HandleCommandLine(OpcService)
  </code></pre>
  <p>以上代码将脚本封装成windows service部署在后台运行，读取数据后保存至mongodb，同时将这条数据发送到消息队列。</p>
  <h2 id="h2-u8BFBu503C"><a name="读值" class="reference-link"></a><span class="header-link octicon octicon-link"></span>读值</h2><p>Java后台是多租户的架构设计，每个公司都有一个mongodb的连接地址，需要管理员启用该公司才能访问MI报表。</p>
  <p><img src="/upload/files/png/cb060a10323f09a826432a8366e1ad81.png" alt="">
  <p>如果未启用：</p>
  <p><img src="/upload/files/png/e3d833b139e24ef3044b9746abe47573.png" alt="">
  <h3 id="h3-mongodb-"><a name="MongoDB连接池" class="reference-link"></a><span class="header-link octicon octicon-link"></span>MongoDB连接池</h3><pre><code class="lang-java">@Component
  @Slf4j
  public class DataSourceManager {


      private static final Map&lt;String, MongoClient&gt; _CONNECTIONS = new HashMap&lt;&gt;();

      CompanyRepository companyRepository;

      @Autowired
      public DataSourceManager(CompanyRepository companyRepository) {
          this.companyRepository = companyRepository;
          log.info("=============init mongodb companies=================");
          List&lt;Company&gt; companies = companyRepository.findCompanyByIsActive(true);
          for (Company company : companies) {
              this.addConnection(company);
              log.info(company.getName() + "init success!");
          }
      }

      synchronized public void addConnection(Company company) {
          if (company.getConnectionString().isEmpty()) {
              throw new NullPointerException("连接地址不能为空");
          }
          String connStr = company.getConnectionString();
          MongoClientURI url = new MongoClientURI(connStr);
          MongoClient mongoClient = new MongoClient(url);

          System.out.println(mongoClient.listDatabaseNames());

          DB db = mongoClient.getDB("mi_" + company.getCode());
          DBCollection coll = db.getCollection("opc_im_data");
          DBObject myDoc = coll.findOne();
          if (_CONNECTIONS.containsKey(company.getCode())) {
              throw new ConnectionAlreadyExist(500, "连接重复");
          } else {
              _CONNECTIONS.put(company.getCode(), mongoClient);
          }
      }


      synchronized public void removeConnection(Company company) {
          _CONNECTIONS.remove(company.getCode());
      }

      public MongoCollection&lt;Document&gt; getConnection(Company company, String collection) {
          if (_CONNECTIONS.containsKey(company.getCode())) {
              MongoClient client = _CONNECTIONS.get(company.getCode());
              MongoDatabase db = client.getDatabase("mi_" + company.getCode());
              return db.getCollection(collection);
          } else {
              throw new NullPointerException("请启动" + company.getName() + "数据库");
          }
      }

  }
  </code></pre>
  <p>在项目启动时会检查已启用的公司，将该公司的数据库连接Client加入到连接池。</p>
  <p>进入Dashboard时候会查询该公司下所有设备，挑第一个设备，从Mongodb中取该设备最新值返回前端报表显示。</p>
  <p>如果在Dashboard切换设备会查询该设备，根据MachineId从MongoDb中查询。</p>
  <p>代码(初版 已废弃)：</p>
  <pre><code class="lang-java">@RequestMapping("/dashboard")
  public ResponseData dashboard(@CompanyInject Company company, @RequestParam(value = "machineId", required = false)
          String machineId) {
      List&lt;BaseMachine&gt; machines = machineRepository.findByCompanyCode(company.getCode());
      Map&lt;String, Object&gt; result = new HashMap&lt;&gt;();
      result.put("machineList", machines);
      result.put("warnRules",new JSONArray());

      if (Objects.isNull(machineId) || machineId.isEmpty()) {
          if (machines.isEmpty()) {
              throw new BusinessException(500, "请维护注塑机数据");
          } else {
              BaseMachine machine = machines.get(0);
              MongoCollection&lt;Document&gt; collection = dataSourceManager.getConnection(company, machine.getDbTable());
              BasicDBObject query = new BasicDBObject("tmMachID", machine.getMachineId());

              MongoCursor&lt;Document&gt; iterator = collection.find(query).sort(new BasicDBObject("create_date", -1)).limit(10).iterator();

              BasicDBList list = new BasicDBList();
              while (iterator.hasNext()) {
                  Document doc = iterator.next();
                  list.add(doc);
              }
              result.put("machineCurrent", machine);
              result.put("lastMachineData", list.get(0));
          }
      } else {
          BaseMachine machine =  machineRepository.findByMachineIdAndCompanyCode(machineId,company.getCode());
          if (Objects.isNull(machine)){
              throw new NullPointerException("未找到machineId");
          }
          MongoCollection&lt;Document&gt; collection = dataSourceManager.getConnection(company, machine.getDbTable());
          BasicDBObject query = new BasicDBObject("tmMachID", machine.getMachineId());

          MongoCursor&lt;Document&gt; iterator = collection.find(query).sort(new BasicDBObject("create_date", -1)).limit(10).iterator();

          BasicDBList list = new BasicDBList();
          while (iterator.hasNext()) {
              Document doc = iterator.next();
              list.add(doc);
          }
          result.put("machineCurrent", machine);
          result.put("lastMachineData", list.get(0));
      }
      Document document = (Document) result.get("lastMachineData");
      String tmCraftID =  document.getString("tmCraftID");
      if (craftRepository.existsBaseCraftByCompanyCodeAndCraftId(company.getCode(),tmCraftID)){
          List&lt;BaseCraftProcess&gt; craftProcesses =  craftProcessRepository.findByCompanyCodeAndCraftId(company.getCode(),tmCraftID);
          result.put("warnRules",craftProcesses);
      }
      return ResponseData.success(result);
  }
  </code></pre>
  <h2 id="h2-u524Du7AEF"><a name="前端" class="reference-link"></a><span class="header-link octicon octicon-link"></span>前端</h2><h3 id="h3--mqtt"><a name="订阅MQTT" class="reference-link"></a><span class="header-link octicon octicon-link"></span>订阅MQTT</h3><p>前台根据 /${machineId} 的规则来订阅，接收消息后更新界面数据。</p>
  <p>代码(初版 已废弃)：</p>
  <pre><code class="lang-js">emqx.subscribeTopic('/' + this.machineCurrent.machineId, data =&gt; {
    that.$globalEventBus.$emit('connection_state', true)
    this.lastMachineData = data
  })
  </code></pre>
  <h2 id="h2-preview"><a name="Preview" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Preview</h2><p><img src="/upload/files/png/9f839a6230be0e9b2255db73f7ead88d.png" alt="">
  <p>NEW VERSION PICTURE<br><img src="/upload/files/png/2c669932f552ba8280ae40908808df12.png" alt="">

markdown_text:
  # OPC协议及MQTT

  ## Requirement

  使用OPC协议读取注塑机设备的值。

  经过考察，当前情况是注塑机没有加入到KEPWARE ，无法通过TCP访问当设备数据。

  所以目前只能在机器本地开发，选用Python开发，使用 OpenOpc 库。

  OpenOpc:  http://openopc.sourceforge.net/

  MQTT 使用 EMQX作为MQ平台：https://www.emqx.io/zh/downloads?os=CentOS

  目前将注塑机数据上传值MQTT，内网环境。

  ## 取值

  OpenOpc 的read方法有点问题，会出现读值慢，无法读下一个值的情况，我猜测这是单线程订阅。使用properties方法可以解决。

  代码(初版 已废弃)：

  ```python
  # ZPF
  # encoding=utf-8
  import datetime
  import json
  import sys
  import time
  import traceback
  from configparser import ConfigParser

  import OpenOPC
  import pywintypes
  import servicemanager
  import win32serviceutil
  import win32service
  import win32event
  import os
  import logging
  import inspect
  from paho.mqtt import client as mqtt_client
  from pymongo import MongoClient
  import win32timezone
  import dateutil.parser

  broker = '172.xxx'
  port = 1883
  topic = ""
  # generate client ID with pub prefix randomly
  client_id = 'xx'
  user = 'xx'
  pwd = 'xx'

  topic_list = ["tmMachID", "tmOperateMode", "tmAlarmState", "tmAlarmID", "tmCraftID", "tmCraftID", "tmCycleTime",
                "tmPlanCount", "tmShotCount", "tmInjHighPress", "tmInjStartPosi", "tmCycleTime", "tmInjTime",
                "tmClpClsTime", "tmClpOpnTime", "tmClpOpnPosi", "tmInjEndPosi", "tmTemp1_Set", "tmTemp2_Set",
                "tmTemp3_Set", "tmTemp4_Set", "tmTemp5_Set", "tmTemp6_Set", "tmTemp1_Current", "tmTemp2_Current",
                "tmTemp3_Current", "tmTemp4_Current", "tmTemp5_Current", "tmTemp6_Current", "tmTempOil_Current"]

  BUFFER = []


  def connect_mqtt(broker, port, client_id, user, pwd):
      def on_connect(client, userdata, flags, rc):
          if rc == 0:
              print("Connected to MQTT Broker!")
          else:
              print("Failed to connect, return code %d\n", rc)

      client = mqtt_client.Client(client_id=client_id, clean_session=False)
      client.username_pw_set(user, pwd)
      client.on_connect = on_connect
      client.connect(broker, port)
      return client


  class Config(object):
      def __init__(self):
          self.conf = ConfigParser()
          self.path = os.path.expanduser('~') + '\\' + '.ciemis_mi'
          self.name = "setting.ini"
          self.dir = self.path + '\\' + self.name
          if not os.path.isdir(self.path):
              print(self.path)
              print("创建...")
              os.makedirs(self.path)
          if not os.path.isfile(self.dir):
              fd = open(self.dir, mode="w", encoding="utf-8")
              fd.close()
              self.init_data()

      # 初始化数据
      def init_data(self):
          self.conf.add_section("TOPIC")
          self.conf.set("TOPIC", "BROKER", broker)
          self.conf.set("TOPIC", "PORT", str(port))
          self.conf.set("TOPIC", "CLIENT_ID", client_id)
          self.conf.set("TOPIC", "URL", topic)
          self.conf.set("TOPIC", "USERNAME", user)
          self.conf.set("TOPIC", "PASSWORD", pwd)
          self.conf.set("TOPIC", "TOPIC_LIST", json.dumps(topic_list))

          self.conf.add_section("MONGODB")
          self.conf.set("MONGODB", "host",
                        "xx")
          self.conf.set("MONGODB", "port", str(3717))
          self.conf.set("MONGODB", "username", "xx")
          self.conf.set("MONGODB", "password", "xx")
          self.conf.set("MONGODB", "db", "xx")
          self.conf.set("MONGODB", "col", "xx")

          self.conf.write(open(self.dir, 'w'))

      def get_topic(self, key: str, load: bool = False):
          self.conf.read(self.dir)
          data = self.conf.get('TOPIC', key)
          if load:
              data = json.loads(data)
          return data

      def get_mongo(self, key: str, load: bool = False):
          self.conf.read(self.dir)
          data = self.conf.get('MONGODB', key)
          if load:
              data = json.loads(data)
          return data


  class OpcService(win32serviceutil.ServiceFramework):
      _svc_name_ = "CiemisOpcSrvice"
      _svc_display_name_ = "Ciemis MI Background Task"
      _svc_description_ = "云智 注塑机 MI Service"

      def __init__(self, args):
          win32serviceutil.ServiceFramework.__init__(self, args)
          self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
          self.logger = self._getLogger()
          self.run = True

      def find_value(self, topic_list, key):
          try:
              if topic_list.index(key) > -1:
                  return True
          except Exception as e:
              return False

      def check_value_change(self, node):
          global BUFFER
          if len(BUFFER) == 0:
              return True
          find_res = None
          for item in BUFFER:
              if item['machine_id'] == node['machine_id'] and item['key'] == node['key']:
                  find_res = item
                  break
          if find_res is None:
              return True
          return find_res['value'] != node['value']

      def save_node(self, node):
          global BUFFER
          if len(BUFFER) == 0:
              return True
          find_index = None
          for index, value in enumerate(BUFFER):
              if value['machine_id'] == node['machine_id'] and value['key'] == node['key']:
                  find_index = index
                  break
          if find_index is None:
              BUFFER.append(node)
          else:
              del BUFFER[find_index]
              BUFFER.append(node)

      def publish(self, client, url, topic_list, col):
          pywintypes.datetime = pywintypes.TimeType
          opc = OpenOPC.client()
          open_server = 'Techmation.OPC.Server.V1'
          opc.connect(opc_server=open_server)
          self.logger.info(opc.info())
          print(self.run)
          print("start ..")
          self.logger.info("start..")
          while self.run:
              try:
                  for machine in opc.list():
                      if opc.properties(machine + ".tmAlarmDateTime")[2][2] != 0:
                          msg = {
                              "create_date": datetime.datetime.utcnow()
                          }
                          for key in opc.list(machine):

                              if self.find_value(topic_list, key):
                                  msg[key] = opc.properties(machine + "." + key, id=2)
                          # 删除 > 10 min
                          now = datetime.datetime.utcnow()
                          now_cut_10 = now - datetime.timedelta(minutes=10)
                          date = dateutil.parser.isoparse(str(now_cut_10))
                          query = {"tmMachID": msg['tmMachID'], 'tmCraftID': msg['tmCraftID'],
                                   'tmShotCount': msg['tmShotCount'], 'create_date': {"$gte": date}}
                          t_url = '/' + msg['tmMachID']
                          result = client.publish(t_url, json.dumps(msg, default=str))
                          status = result[0]
                          if status == 0:
                              self.logger.info(
                                  "Send {msg} to topic {topic}".format(msg=json.dumps(msg, default=str),
                                                                       topic=t_url))
                              col.delete_many(query)
                              col.insert_one(msg)
                          else:
                              self.logger.error(
                                  "Failed to send message to topic {topic}".format(topic=t_url))
              except  Exception as e:
                  traceback.print_exc()
                  self.logger.exception(f"main exception: {str(e)}")
                  self.logger.error("TimeoutError occured")
              time.sleep(10)

      def _getLogger(self):
          logger = logging.getLogger('[CiemisOpcSrvice]')

          handler = logging.FileHandler("E:\ciemis\service.log")

          formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
          handler.setFormatter(formatter)

          logger.addHandler(handler)
          logger.setLevel(logging.INFO)

          return logger

      def SvcDoRun(self):
          self.logger.info("service is running")
          config = Config()
          self.logger.info("config check ..")
          self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
          client_id = config.get_topic("client_id")
          broker = config.get_topic("broker")
          url = config.get_topic("url")
          port = config.get_topic("port")
          user = config.get_topic("username")
          pwd = config.get_topic("password")
          self.logger.info(broker)
          self.logger.info(url)
          self.logger.info(port)
          self.logger.info(user)
          self.logger.info(pwd)
          self.logger.info(topic_list)
          self.logger.info("config check ok..")

          mongo_host = "xx"
          mongo_port = 3717
          mongo_username = 'xx'
          mongo_password = 'xx'
          mongo_db = 'xx'
          mongo_col = 'xx'
          self.logger.info(f"host:{mongo_host}")
          self.logger.info(f"mongo_db:{mongo_db}")
          self.logger.info(f"mongo_col:{mongo_col}")

          client = MongoClient(host=mongo_host, port=int(mongo_port), username=mongo_username,
                               password=mongo_password)  # 生成mongodb对象
          db = client[mongo_db]
          col = db[mongo_col]
          self.ReportServiceStatus(win32service.SERVICE_RUNNING)

          try:
              client = connect_mqtt(broker, int(port), client_id, user, pwd)
              client.loop_start()
              self.logger.info("connect ok..")
              self.publish(client, url, topic_list, col)
              win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
          except Exception as e:
              traceback.print_exc()
              self.logger.info(e)

      def SvcStop(self, message: str = "service is stop"):
          self.logger.info(message)
          self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
          win32event.SetEvent(self.hWaitStop)
          self.run = False


  if __name__ == '__main__':
      if len(sys.argv) == 1:
          servicemanager.Initialize()
          servicemanager.PrepareToHostSingle(OpcService)
          servicemanager.StartServiceCtrlDispatcher()
      else:
          win32serviceutil.HandleCommandLine(OpcService)

  ```

  以上代码将脚本封装成windows service部署在后台运行，读取数据后保存至mongodb，同时将这条数据发送到消息队列。

  ## 读值

  Java后台是多租户的架构设计，每个公司都有一个mongodb的连接地址，需要管理员启用该公司才能访问MI报表。

  ![](/upload/files/png/cb060a10323f09a826432a8366e1ad81.png)

  如果未启用：

  ![](/upload/files/png/e3d833b139e24ef3044b9746abe47573.png)

  ### MongoDB连接池

  ```java
  @Component
  @Slf4j
  public class DataSourceManager {


      private static final Map<String, MongoClient> _CONNECTIONS = new HashMap<>();

      CompanyRepository companyRepository;

      @Autowired
      public DataSourceManager(CompanyRepository companyRepository) {
          this.companyRepository = companyRepository;
          log.info("=============init mongodb companies=================");
          List<Company> companies = companyRepository.findCompanyByIsActive(true);
          for (Company company : companies) {
              this.addConnection(company);
              log.info(company.getName() + "init success!");
          }
      }

      synchronized public void addConnection(Company company) {
          if (company.getConnectionString().isEmpty()) {
              throw new NullPointerException("连接地址不能为空");
          }
          String connStr = company.getConnectionString();
          MongoClientURI url = new MongoClientURI(connStr);
          MongoClient mongoClient = new MongoClient(url);

          System.out.println(mongoClient.listDatabaseNames());

          DB db = mongoClient.getDB("mi_" + company.getCode());
          DBCollection coll = db.getCollection("opc_im_data");
          DBObject myDoc = coll.findOne();
          if (_CONNECTIONS.containsKey(company.getCode())) {
              throw new ConnectionAlreadyExist(500, "连接重复");
          } else {
              _CONNECTIONS.put(company.getCode(), mongoClient);
          }
      }


      synchronized public void removeConnection(Company company) {
          _CONNECTIONS.remove(company.getCode());
      }

      public MongoCollection<Document> getConnection(Company company, String collection) {
          if (_CONNECTIONS.containsKey(company.getCode())) {
              MongoClient client = _CONNECTIONS.get(company.getCode());
              MongoDatabase db = client.getDatabase("mi_" + company.getCode());
              return db.getCollection(collection);
          } else {
              throw new NullPointerException("请启动" + company.getName() + "数据库");
          }
      }

  }
  ```

  在项目启动时会检查已启用的公司，将该公司的数据库连接Client加入到连接池。

  进入Dashboard时候会查询该公司下所有设备，挑第一个设备，从Mongodb中取该设备最新值返回前端报表显示。

  如果在Dashboard切换设备会查询该设备，根据MachineId从MongoDb中查询。

  代码(初版 已废弃)：

  ```java
  @RequestMapping("/dashboard")
  public ResponseData dashboard(@CompanyInject Company company, @RequestParam(value = "machineId", required = false)
          String machineId) {
      List<BaseMachine> machines = machineRepository.findByCompanyCode(company.getCode());
      Map<String, Object> result = new HashMap<>();
      result.put("machineList", machines);
      result.put("warnRules",new JSONArray());

      if (Objects.isNull(machineId) || machineId.isEmpty()) {
          if (machines.isEmpty()) {
              throw new BusinessException(500, "请维护注塑机数据");
          } else {
              BaseMachine machine = machines.get(0);
              MongoCollection<Document> collection = dataSourceManager.getConnection(company, machine.getDbTable());
              BasicDBObject query = new BasicDBObject("tmMachID", machine.getMachineId());

              MongoCursor<Document> iterator = collection.find(query).sort(new BasicDBObject("create_date", -1)).limit(10).iterator();

              BasicDBList list = new BasicDBList();
              while (iterator.hasNext()) {
                  Document doc = iterator.next();
                  list.add(doc);
              }
              result.put("machineCurrent", machine);
              result.put("lastMachineData", list.get(0));
          }
      } else {
          BaseMachine machine =  machineRepository.findByMachineIdAndCompanyCode(machineId,company.getCode());
          if (Objects.isNull(machine)){
              throw new NullPointerException("未找到machineId");
          }
          MongoCollection<Document> collection = dataSourceManager.getConnection(company, machine.getDbTable());
          BasicDBObject query = new BasicDBObject("tmMachID", machine.getMachineId());

          MongoCursor<Document> iterator = collection.find(query).sort(new BasicDBObject("create_date", -1)).limit(10).iterator();

          BasicDBList list = new BasicDBList();
          while (iterator.hasNext()) {
              Document doc = iterator.next();
              list.add(doc);
          }
          result.put("machineCurrent", machine);
          result.put("lastMachineData", list.get(0));
      }
      Document document = (Document) result.get("lastMachineData");
      String tmCraftID =  document.getString("tmCraftID");
      if (craftRepository.existsBaseCraftByCompanyCodeAndCraftId(company.getCode(),tmCraftID)){
          List<BaseCraftProcess> craftProcesses =  craftProcessRepository.findByCompanyCodeAndCraftId(company.getCode(),tmCraftID);
          result.put("warnRules",craftProcesses);
      }
      return ResponseData.success(result);
  }
  ```

  ## 前端

  ### 订阅MQTT

  前台根据 /${machineId} 的规则来订阅，接收消息后更新界面数据。

  代码(初版 已废弃)：

  ```js
  emqx.subscribeTopic('/' + this.machineCurrent.machineId, data => {
    that.$globalEventBus.$emit('connection_state', true)
    this.lastMachineData = data
  })
  ```

  ## Preview

  ![](/upload/files/png/9f839a6230be0e9b2255db73f7ead88d.png)

  NEW VERSION PICTURE
  ![](/upload/files/png/2c669932f552ba8280ae40908808df12.png)


title: Linux 操作记录 OpenVpn RabbitMQ Docker Nginx
intro: Linux 操作记录 OpenVpn RabbitMQ Docker Nginx
created_time: 2022-11-25 08:00:20.372608
html_text:
  <h2 id="h2-openvpn"><a name="Openvpn" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Openvpn</h2><pre><code class="lang-shell">sudo apt update
  sudo apt install openvpn -y
  mkdir /opt/openvpn-client
  sudo apt install screen
  cd /opt/openvpn-client
  #xshell
  sudo apt-get install lrzsz
  rz #upload ovpn
  screen -S openvpnclient
  screen -r openvpnclient
  openvpn --config ciemis.ovpn --auth-user-pass pwd
  </code></pre>
  <p><a href="https://www.ivpn.net/knowledgebase/linux/linux-autostart-openvpn-in-systemd-ubuntu/">https://www.ivpn.net/knowledgebase/linux/linux-autostart-openvpn-in-systemd-ubuntu/</a></p>
  <h2 id="h2-docker"><a name="Docker" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Docker</h2><p><a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a></p>
  <p>Docker0 网卡 172网段与openvpn冲突。</p>
  <blockquote>
  <ul>
  <li>Edit /lib/systemd/system/docker.service before start Docker. Add <code>--bip "192.168.1.1/24"</code> at the end of line <code>ExecStart=/usr/bin/dockerd</code>.</li><li><code>systemctl daemon-reload</code></li><li><code>systemctl start docker</code></li></ul>
  </blockquote>
  <h2 id="h2-nginx"><a name="Nginx" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Nginx</h2><pre><code class="lang-shell">sudo apt install nginx
  </code></pre>
  <h2 id="h2-rabbitmq"><a name="RabbitMQ" class="reference-link"></a><span class="header-link octicon octicon-link"></span>RabbitMQ</h2><pre><code class="lang-shell">docker pull docker.io/library/rabbitmq:3.7.7-management
  docker run -d --hostname rabbitmq_server --name mes_mq -p 5673:5672  15672:15672   -p 5674:5674  rabbitmq:3.7.7-management 
  root@FriendlyELEC:~# docker update  --restart=always 1d7f29f8601c
  </code></pre>

markdown_text:
  ## Openvpn 

  ```shell
  sudo apt update
  sudo apt install openvpn -y
  mkdir /opt/openvpn-client
  sudo apt install screen
  cd /opt/openvpn-client
  #xshell
  sudo apt-get install lrzsz
  rz #upload ovpn
  screen -S openvpnclient
  screen -r openvpnclient
  openvpn --config ciemis.ovpn --auth-user-pass pwd
  ```

  https://www.ivpn.net/knowledgebase/linux/linux-autostart-openvpn-in-systemd-ubuntu/

  ## Docker

  https://docs.docker.com/engine/install/ubuntu/

  Docker0 网卡 172网段与openvpn冲突。

  > - Edit /lib/systemd/system/docker.service before start Docker. Add `--bip "192.168.1.1/24"` at the end of line `ExecStart=/usr/bin/dockerd`.
  > - `systemctl daemon-reload`
  > - `systemctl start docker`



  ## Nginx

  ```shell
  sudo apt install nginx
  ```



  ## RabbitMQ

  ```shell
  docker pull docker.io/library/rabbitmq:3.7.7-management
  docker run -d --hostname rabbitmq_server --name mes_mq -p 5673:5672  15672:15672   -p 5674:5674  rabbitmq:3.7.7-management 
  root@FriendlyELEC:~# docker update  --restart=always 1d7f29f8601c
  ```


title: robocopy
intro: robocopy
created_time: 2023-02-22 06:25:30.137142
html_text:
  <pre><code class="lang-bash">robocopy SOURCE DEST /mir /xd node_modules
  </code></pre>
  <p><img src="/upload/files/png/5eaade4ed8fecc8f33fbc601365dc240.png" alt="">
  <p><a href="https://superuser.com/questions/940804/copy-large-number-of-files-but-exclude-certain-sub-folders-on-windows" title="Copy large number of files but exclude certain sub folders on Windows">Copy large number of files but exclude certain sub folders on Windows</a></p>

markdown_text:
  ```bash
  robocopy SOURCE DEST /mir /xd node_modules
  ```
  ![](/upload/files/png/5eaade4ed8fecc8f33fbc601365dc240.png)

  [Copy large number of files but exclude certain sub folders on Windows](https://superuser.com/questions/940804/copy-large-number-of-files-but-exclude-certain-sub-folders-on-windows "Copy large number of files but exclude certain sub folders on Windows")


title: Python 函数动态调用
created_time: 2023-03-15 02:00:43.364515
intro:
  Requirement  Http -> Aliyun FC -> Python Handle -> dynamic condition -> Template  发票文件OCR解析识别后从JSON文件从中提取需要的信息。  一个供应商一个发票，有多个供应商，根据供应商名称来识别，我这里的做法是一个供应商一个py文件，文件名用供应商代码来命名。

html_text:
  <h3 id="h3-requirement"><a name="Requirement" class="reference-link"></a><span class="header-link octicon octicon-link"></span>Requirement</h3><p>Http -&gt; Aliyun FC -&gt; Python Handle -&gt; dynamic condition -&gt; Template</p>
  <p>发票文件OCR解析识别后从JSON文件从中提取需要的信息。</p>
  <p>一个供应商一个发票，有多个供应商，根据供应商名称来识别，我这里的做法是一个供应商一个py文件，文件名用供应商代码来命名。</p>
  <p>在写的时候我想避免以下代码结构：</p>
  <pre><code class="lang-python">import 供应商1.py as s1
  import 供应商2.py as s2
  if supplier_code == '供应商1':
      s1.parse()
  elif: supplier_code == '供应商2':
      s2.parse()
  ....
  </code></pre>
  <p>这种方式明显显得不够灵活和动态，于是经过查阅，发现Python可以动态导入模块 于是有了下面代码：</p>
  <p><img src="/upload/files/png/d654b66f9c3d3d2c2fed2351b94ea38b.png" alt="">
  <pre><code class="lang-python">import importlib
  import common as util
  import traceback

  def dispatch(plant:str,supplier:str,file_extension:str,ocr_result_obj):
      try:
          dynamic_module = importlib.import_module('template.' + supplier)
          func_call = getattr(dynamic_module, "parse")
          return func_call(plant,supplier,file_extension,ocr_result_obj,util)
      except Exception as e:
          traceback.print_exc()
      except AttributeError as ae:
          raise ModuleNotFoundError("调用失败: {0}".format(ae))
      except TypeError as te:
          raise ModuleNotFoundError("类型异常: {0}".format(te))
      except ModuleNotFoundError as e1:
          raise ModuleNotFoundError("没有找到模块: {0}".format(e1))
  </code></pre>
  <p>import_module 动态导入，getattr 动态调用模块函数，以此方法达到如果新增供应商解析模板，只要在template目录下新增一个名称对应的py文件，定义parse方法即可，其他的都不用管。</p>
  <pre><code class="lang-python">import re

  def get_ln(text:str):
      pattern = re.compile(r'-?[1-9]\d*')
      return pattern.search(text).group()

  def get_po(text:str):
      pattern = re.compile(r'P\s*X\s*Z\s*[0-9]{5}')
      result = pattern.match(text)
      if result is not None:
          return True
      else:
          return False

  def get_invoice_amount(text:str):
      pattern = re.compile(r'00\s*USD$')
      result = pattern.search(text)
      if result is not None:
          return True
      else:
          return False

  def get_ip_no(text:str):
      pattern = re.compile(r'^\bde\s*.*\s*[0-9]$', re.IGNORECASE)
      result = pattern.search(text)
      if result is not None:
          return True
      else:
          return False

  def parse(plant,supplier,file_extension,ocr_result_obj,util):
      # print("收到参数:{0} {1}".format(plant,str(ocr_result_obj['Headers'])))
      PEOPERTY = ""

      results = None
      if file_extension == '.pdf':
          PEOPERTY = "Text"
          results = ocr_result_obj['Body']['Data']['Results']
      else:
          PEOPERTY = "Word"
          results = ocr_result_obj['Body']['Data']['WordsInfo']

      po = util.find_match_by(results,get_po,PEOPERTY)
      invoice_amount =  util.find_match_by(results,get_invoice_amount,PEOPERTY)
      lp_no = util.find_match_by(results,get_ip_no,PEOPERTY)
      ln = util.find_result_by_text('Invoice number',results,get_ln,PEOPERTY)

      if po is not None:
          po = po.replace(' ','')
      if lp_no is not None:
          lp_no = util.get_match_number(lp_no)
      if invoice_amount is not None:
          invoice_amount = invoice_amount.replace(' ','').replace('，','').replace(',','').replace('USD','')

      # return [ln,po,invoice_amount,lp_no]
      return util.wrapper(ln,invoice_amount,lp_no,po,None,None)
  </code></pre>
  <p>弊端：</p>
  <p>被动态调用的函数不能再当前文件引入其他类库，会报错，我通过函数回调来解决，但增加了一些代码的复杂度。</p>
  <p>​        </p>

markdown_text:
  ### Requirement

  Http -> Aliyun FC -> Python Handle -> dynamic condition -> Template

  发票文件OCR解析识别后从JSON文件从中提取需要的信息。

  一个供应商一个发票，有多个供应商，根据供应商名称来识别，我这里的做法是一个供应商一个py文件，文件名用供应商代码来命名。

  在写的时候我想避免以下代码结构：

  ```python
  import 供应商1.py as s1
  import 供应商2.py as s2
  if supplier_code == '供应商1':
      s1.parse()
  elif: supplier_code == '供应商2':
      s2.parse()
  ....
  ```

  这种方式明显显得不够灵活和动态，于是经过查阅，发现Python可以动态导入模块 于是有了下面代码：

  ![](/upload/files/png/d654b66f9c3d3d2c2fed2351b94ea38b.png)

  ```python
  import importlib
  import common as util
  import traceback

  def dispatch(plant:str,supplier:str,file_extension:str,ocr_result_obj):
      try:
          dynamic_module = importlib.import_module('template.' + supplier)
          func_call = getattr(dynamic_module, "parse")
          return func_call(plant,supplier,file_extension,ocr_result_obj,util)
      except Exception as e:
          traceback.print_exc()
      except AttributeError as ae:
          raise ModuleNotFoundError("调用失败: {0}".format(ae))
      except TypeError as te:
          raise ModuleNotFoundError("类型异常: {0}".format(te))
      except ModuleNotFoundError as e1:
          raise ModuleNotFoundError("没有找到模块: {0}".format(e1))


  ```

  import_module 动态导入，getattr 动态调用模块函数，以此方法达到如果新增供应商解析模板，只要在template目录下新增一个名称对应的py文件，定义parse方法即可，其他的都不用管。

  ```python
  import re

  def get_ln(text:str):
      pattern = re.compile(r'-?[1-9]\d*')
      return pattern.search(text).group()

  def get_po(text:str):
      pattern = re.compile(r'P\s*X\s*Z\s*[0-9]{5}')
      result = pattern.match(text)
      if result is not None:
          return True
      else:
          return False

  def get_invoice_amount(text:str):
      pattern = re.compile(r'00\s*USD$')
      result = pattern.search(text)
      if result is not None:
          return True
      else:
          return False

  def get_ip_no(text:str):
      pattern = re.compile(r'^\bde\s*.*\s*[0-9]$', re.IGNORECASE)
      result = pattern.search(text)
      if result is not None:
          return True
      else:
          return False

  def parse(plant,supplier,file_extension,ocr_result_obj,util):
      # print("收到参数:{0} {1}".format(plant,str(ocr_result_obj['Headers'])))
      PEOPERTY = ""

      results = None
      if file_extension == '.pdf':
          PEOPERTY = "Text"
          results = ocr_result_obj['Body']['Data']['Results']
      else:
          PEOPERTY = "Word"
          results = ocr_result_obj['Body']['Data']['WordsInfo']

      po = util.find_match_by(results,get_po,PEOPERTY)
      invoice_amount =  util.find_match_by(results,get_invoice_amount,PEOPERTY)
      lp_no = util.find_match_by(results,get_ip_no,PEOPERTY)
      ln = util.find_result_by_text('Invoice number',results,get_ln,PEOPERTY)

      if po is not None:
          po = po.replace(' ','')
      if lp_no is not None:
          lp_no = util.get_match_number(lp_no)
      if invoice_amount is not None:
          invoice_amount = invoice_amount.replace(' ','').replace('，','').replace(',','').replace('USD','')
      
      # return [ln,po,invoice_amount,lp_no]
      return util.wrapper(ln,invoice_amount,lp_no,po,None,None)
  ```

  弊端：

  被动态调用的函数不能再当前文件引入其他类库，会报错，我通过函数回调来解决，但增加了一些代码的复杂度。

  ​		


title: Openvpn 重续证书
intro: Openvpn 重续证书
created_time: 2023-03-17 07:53:54.921854
html_text:
  <pre><code class="lang-bash">Xshell 7 (Build 0093)
  Copyright (c) 2020 NetSarang Computer, Inc. All rights reserved.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.25:22...
  Could not connect to '172.17.21.25' (port 22): Connection failed.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.25:22...
  Could not connect to '172.17.21.25' (port 22): Connection failed.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.26:22...
  Connection established.
  To escape to local shell, press Ctrl+Alt+].

  WARNING! The remote SSH server rejected X11 forwarding request.

  Welcome to Alibaba Cloud Elastic Compute Service !

  Last login: Wed Mar 15 16:39:34 2023 from 172.17.21.25
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  The authenticity of host '172.17.21.25 (172.17.21.25)' can't be established.
  ECDSA key fingerprint is SHA256:VY15Lar9HjcUt2OwYClxK4L2AnLJBAh/GheCVG3iep4.
  Are you sure you want to continue connecting (yes/no/[fingerprint])? 
  Host key verification failed.
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  The authenticity of host '172.17.21.25 (172.17.21.25)' can't be established.
  ECDSA key fingerprint is SHA256:VY15Lar9HjcUt2OwYClxK4L2AnLJBAh/GheCVG3iep4.
  Are you sure you want to continue connecting (yes/no/[fingerprint])? y
  Please type 'yes', 'no' or the fingerprint: y
  Please type 'yes', 'no' or the fingerprint: yes
  Warning: Permanently added '172.17.21.25' (ECDSA) to the list of known hosts.
  root@172.17.21.25's password: 
  Last login: Fri Nov 25 15:00:03 2022 from 192.168.112.30

  Welcome to Alibaba Cloud Elastic Compute Service !

  [root@t2 ~]# ls
  centos7-vpn.sh  client.tar.gz   epel-release-latest-7.noarch.rpm  influxdb-1.7.6.x86_64.rpm  master.zip  telegraf-1.15.3-1.x86_64.rpm
  client          controlsfx.jar  grafana-7.1.0-1.x86_64.rpm        influxdb-1.8.3.x86_64.rpm  nginx
  [root@t2 ~]# cd /opt/
  [root@t2 opt]# ls
  project
  [root@t2 opt]# cd project/
  [root@t2 project]# ls
  db.sqlite3  manage.py  nohup.out  Pipfile  Pipfile.lock  requirements.txt  start.sh  static  templates  vpn  vpn_ms
  [root@t2 project]# cd vpn
  [root@t2 vpn]# ls
  admin.py  apps.py  context_processors.py  __init__.py  migrations  models.py  __pycache__  tests.py  views.py
  [root@t2 vpn]# cd /etc/
  [root@t2 etc]# ls
  adjtime            cron.weekly              gdbinit.d    issue.net       motd                       polkit-1          rsyslog.conf         sysctl.d
  adjtime.rpmsave    crypttab                 GeoIP.conf   kernel          mtab                       popt.d            rsyslog.d            systemd
  aliases            csh.cshrc                gnupg        krb5.conf       my.cnf                     postfix           rwtab                system-lsb
  aliases.db         csh.login                grafana      krb5.conf.d     my.cnf.d                   ppp               rwtab.d              system-release
  alinux-release     dbus-1                   GREP_COLORS  ld.so.cache     netconfig                  prelink.conf.d    sasl2                telegraf
  alternatives       default                  groff        ld.so.conf      NetworkManager             printcap          securetty            terminfo
  anacrontab         depmod.d                 group        ld.so.conf.d    networks                   profile           security             tmpfiles.d
  anolis-release     dhcp                     group-       libaudit.conf   nfs.conf                   profile.d         selinux              tuned
  asound.conf        DIR_COLORS               grub2.cfg    libnl           nfsmount.conf              protocols         services             udev
  at.deny            DIR_COLORS.256color      grub.d       libreport       nscd.conf                  python            sestatus.conf        update-motd.d
  audisp             DIR_COLORS.lightbgcolor  gshadow      libuser.conf    nsswitch.conf              rc0.d             shadow               vconsole.conf
  audit              docker                   gshadow-     locale.conf     nsswitch.conf.bak          rc1.d             shadow-              vimrc
  bash_completion.d  dracut.conf              gss          localtime       nsswitch.conf.rpmnew       rc2.d             shells               virc
  bashrc             dracut.conf.d            gssproxy     login.defs      ntp.conf                   rc3.d             skel                 wgetrc
  binfmt.d           e2fsck.conf              host.conf    logrotate.conf  oci-register-machine.conf  rc4.d             ssh                  wireguard
  centos-release     environment              hostname     logrotate.d     oci-umount                 rc5.d             ssl                  wpa_supplicant
  chkconfig.d        ethertypes               hosts        lsb-release.d   oci-umount.conf            rc6.d             statetab             X11
  chrony.conf        exports                  hosts.allow  lvm             openldap                   rc.d              statetab.d           xdg
  chrony.keys        exports.d                hosts.deny   machine-id      openvpn                    rc.local          subgid               xinetd.d
  cloud              favicon.png              idmapd.conf  magic           opt                        redhat-release    subuid               yum
  containers         filesystems              image-id     mailcap         os-release                 request-key.conf  sudo.conf            yum.conf
  cron.d             firewalld                influxdb     mail.rc         pam.d                      request-key.d     sudoers              yum.repos.d
  cron.daily         fonts                    init.d       man_db.conf     passwd                     resolv.conf       sudoers.d
  cron.deny          fstab                    inittab      mime.types      passwd-                    resolv.conf.save  sudo-ldap.conf
  cron.hourly        fuse.conf                inputrc      mke2fs.conf     pkcs11                     rpc               sysconfig
  cron.monthly       gcrypt                   iproute2     modprobe.d      pki                        rpm               sysctl.conf
  crontab            gdbinit                  issue        modules-load.d  pm                         rsyncd.conf       sysctl.conf.rpmsave
  [root@t2 etc]# cd op
  openldap/ openvpn/  opt/      
  [root@t2 etc]# cd op
  openldap/ openvpn/  opt/      
  [root@t2 etc]# cd openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# openssl x509 -noout -text in server.crt 
  unknown option in
  usage: x509 args
   -inform arg     - input format - default PEM (one of DER, NET or PEM)
   -outform arg    - output format - default PEM (one of DER, NET or PEM)
   -keyform arg    - private key format - default PEM
   -CAform arg     - CA format - default PEM
   -CAkeyform arg  - CA key format - default PEM
   -in arg         - input file - default stdin
   -out arg        - output file - default stdout
   -passin arg     - private key password source
   -serial         - print serial number value
   -subject_hash   - print subject hash value
   -subject_hash_old   - print old-style (MD5) subject hash value
   -issuer_hash    - print issuer hash value
   -issuer_hash_old    - print old-style (MD5) issuer hash value
   -hash           - synonym for -subject_hash
   -subject        - print subject DN
   -issuer         - print issuer DN
   -email          - print email address(es)
   -startdate      - notBefore field
   -enddate        - notAfter field
   -purpose        - print out certificate purposes
   -dates          - both Before and After dates
   -modulus        - print the RSA key modulus
   -pubkey         - output the public key
   -fingerprint    - print the certificate fingerprint
   -alias          - output certificate alias
   -noout          - no certificate output
   -ocspid         - print OCSP hash values for the subject name and public key
   -ocsp_uri       - print OCSP Responder URL(s)
   -trustout       - output a "trusted" certificate
   -clrtrust       - clear all trusted purposes
   -clrreject      - clear all rejected purposes
   -addtrust arg   - trust certificate for a given purpose
   -addreject arg  - reject certificate for a given purpose
   -setalias arg   - set certificate alias
   -days arg       - How long till expiry of a signed certificate - def 30 days
   -checkend arg   - check whether the cert expires in the next arg seconds
                     exit 1 if so, 0 if not
   -signkey arg    - self sign cert with arg
   -x509toreq      - output a certification request object
   -req            - input is a certificate request, sign and output.
   -CA arg         - set the CA certificate, must be PEM format.
   -CAkey arg      - set the CA key, must be PEM format
                     missing, it is assumed to be in the CA file.
   -CAcreateserial - create serial number file if it does not exist
   -CAserial arg   - serial file
   -set_serial     - serial number to use
   -text           - print the certificate in text form
   -C              - print out C code forms
   -&lt;dgst&gt;         - digest to use, see openssl dgst -h output for list
   -extfile        - configuration file with X509V3 extensions to add
   -extensions     - section from config file with X509V3 extensions to add
   -clrext         - delete extensions before signing and input certificate
   -nameopt arg    - various certificate name options
   -engine e       - use engine e, possibly a hardware device.
   -certopt arg    - various certificate text options
   -checkhost host - check certificate matches "host"
   -checkemail email - check certificate matches "email"
   -checkip ipaddr - check certificate matches "ipaddr"
  [root@t2 openvpn]# openssl x509 -noout -text -in server.crt 
  Certificate:
      Data:
          Version: 3 (0x2)
          Serial Number:
              95:a6:bd:d4:02:d2:63:dc:bd:08:39:14:6e:a3:3c:94
      Signature Algorithm: sha256WithRSAEncryption
          Issuer: CN=micvs
          Validity
              Not Before: Dec 12 04:50:26 2020 GMT
              Not After : Mar 17 04:50:26 2023 GMT
          Subject: CN=micvs_server
          Subject Public Key Info:
              Public Key Algorithm: rsaEncryption
                  Public-Key: (2048 bit)
                  Modulus:
                      00:d8:2e:eb:0a:e8:e1:9f:87:b8:79:61:ab:96:75:
                      be:87:8e:33:74:80:d0:0b:aa:8e:7d:56:39:04:e4:
                      89:d4:38:a9:77:fc:c0:60:27:f7:1c:0f:cc:cb:30:
                      2f:c9:92:ed:06:06:c7:b3:5b:9e:fc:c3:f5:ca:8a:
                      d0:8d:ff:28:e4:29:03:41:5b:fd:8b:97:f9:d1:17:
                      2f:ce:37:8e:36:dd:d9:e0:94:7e:a6:16:9f:2e:98:
                      13:13:20:05:fa:3a:1a:17:55:87:0b:68:51:4c:7f:
                      64:32:13:94:5e:31:3a:12:5e:02:33:1e:67:99:a7:
                      0f:ee:2e:a9:5c:0d:d4:31:e4:8b:8f:ea:70:3d:1b:
                      d9:81:5d:2a:5f:6f:1c:d2:89:f9:e7:85:01:ee:6d:
                      ec:64:4e:5d:1b:91:e7:f7:98:eb:24:85:fe:bc:7c:
                      28:3a:2f:8a:ca:bf:50:53:db:66:59:4a:fa:2e:eb:
                      1f:81:f5:64:86:f0:1e:a3:87:52:95:59:6d:39:5d:
                      0b:71:bc:48:70:d2:cc:7f:bd:0c:2e:2d:0e:7a:0b:
                      01:3e:1b:63:3d:d8:af:8c:42:50:be:aa:6e:11:a9:
                      0f:10:71:38:22:95:42:83:d6:02:c3:2d:b2:83:01:
                      11:18:c9:b9:eb:28:72:b6:72:fe:e0:2c:98:0c:01:
                      8a:9d
                  Exponent: 65537 (0x10001)
          X509v3 extensions:
              X509v3 Basic Constraints: 
                  CA:FALSE
              X509v3 Subject Key Identifier: 
                  5C:E3:7F:82:65:F7:D7:A6:09:05:4F:88:09:E9:C2:75:5F:5F:B4:CE
              X509v3 Authority Key Identifier: 
                  keyid:51:93:EB:47:9A:69:76:9B:5D:46:5F:EC:C6:8D:50:2D:18:6D:8D:11
                  DirName:/CN=micvs
                  serial:8F:B4:36:E7:E5:0C:1A:B6

              X509v3 Extended Key Usage: 
                  TLS Web Server Authentication
              X509v3 Key Usage: 
                  Digital Signature, Key Encipherment
              X509v3 Subject Alternative Name: 
                  DNS:micvs_server
      Signature Algorithm: sha256WithRSAEncryption
           64:e1:1c:b9:c4:58:1b:69:93:d6:be:38:39:b1:ca:c1:41:89:
           e4:12:16:b8:b7:66:6a:15:1e:75:41:d9:93:af:d2:ff:d0:78:
           47:0a:cd:3d:de:cf:fc:26:f7:d1:76:6c:fc:b4:9c:78:53:b9:
           02:0a:35:fd:83:78:4e:ef:b4:ba:60:93:b9:64:b6:fb:25:9c:
           2a:53:ec:ec:b4:6d:a5:5e:15:46:c1:c1:6f:2c:99:cf:36:91:
           10:07:05:27:91:ff:20:3e:29:8f:20:b3:58:34:04:b6:d0:b1:
           e9:a3:27:d8:81:77:cf:f0:19:8e:5c:a1:5e:39:d1:a3:ba:e4:
           bf:9e:0d:57:a3:6c:b4:b0:10:81:29:08:d0:55:35:b8:72:b9:
           b4:2a:04:18:99:e2:08:70:6b:1f:fc:ee:2f:94:4c:59:0c:e9:
           66:1a:fa:a5:e6:6f:a2:f3:66:18:22:da:17:a2:a8:76:01:06:
           98:f7:a2:8a:b5:1e:ca:b0:ae:6f:eb:f4:1a:6e:e2:89:cd:87:
           39:67:28:79:f6:f4:86:7d:de:5e:b1:10:3c:a7:72:69:fd:58:
           9d:1e:9f:54:e2:06:3c:a7:39:4c:a5:d0:4b:12:15:8a:c7:95:
           06:41:33:f4:74:0d:68:7c:5e:f5:67:f2:d1:cc:fc:e3:aa:14:
           49:09:5a:7b
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# mkdir cert_2023-208
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  [root@t2 cert_2023-208]# cp -r /etc/open
  openldap/ openvpn/  
  [root@t2 cert_2023-208]# cp -r /etc/open
  openldap/ openvpn/  
  [root@t2 cert_2023-208]# cp -r /etc/openvpn/easy-rsa ./
  [root@t2 cert_2023-208]# ls
  easy-rsa
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# ./easyrsa3/
  easyrsa     pki/        x509-types/ 
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# rm -rf pki/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa init-pki

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars

  init-pki complete; you may now create a CA or requests.
  Your newly created PKI dir is: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki


  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa --batch build-ca nopass
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating RSA private key, 2048 bit long modulus
  ...................+++
  .......................................................+++
  e is 65537 (0x10001)

  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopassEASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass^C
  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating a 2048 bit RSA private key
  ............+++
  ................................................................+++
  writing new private key to '/etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-14928.jQN2WT/tmp.EkeT0G'
  -----
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-14928.jQN2WT/tmp.DmWly1
  Check that the request matches the signature
  Signature ok
  The Subject's Distinguished Name is as follows
  commonName            :ASN.1 12:'server'
  Certificate is to be certified until Mar 14 07:15:04 2033 GMT (3650 days)

  Write out database with 1 new entries
  Data Base Updated

  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full client nopass

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating a 2048 bit RSA private key
  ......+++
  .................................+++
  writing new private key to '/etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15028.kmnt4D/tmp.rZUlnF'
  -----
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15028.kmnt4D/tmp.cqwOFp
  Check that the request matches the signature
  Signature ok
  The Subject's Distinguished Name is as follows
  commonName            :ASN.1 12:'client'
  Certificate is to be certified until Mar 14 07:15:19 2033 GMT (3650 days)

  Write out database with 1 new entries
  Data Base Updated

  [root@t2 easyrsa3]# EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15103.TnYFbh/tmp.RbHEgL

  An updated CRL has been created.
  CRL file: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/crl.pem


  [root@t2 easyrsa3]# cp pki/ca.crt pki/private/ca.key pki/issued/server.crt pki/private/server.key pki/crl.pem ../../
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd ..
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# chown nobody:nobody crl.pem 
  [root@t2 cert_2023-208]#  openssl verify -CAfile ca.crt -purpose sslserver server.crt
  server.crt: OK
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd  client/
  [root@t2 client]# ls
  [root@t2 client]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# l
  -bash: l: command not found
  [root@t2 server]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# ls
  [root@t2 server]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# cd ..
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# zip ./* Expires_2023年3月17日.zip
      zip warning: missing end signature--probably not a zip file (did you
      zip warning: remember to use binary mode when you transferred it?)
      zip warning: (if you are trying to read a damaged archive try -F)

  zip error: Zip file structure invalid (./ca.crt)
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# zip  Expires_2023年3月17日.zip ./*
    adding: ca.crt (deflated 27%)
    adding: dh.pem (deflated 18%)
    adding: server.crt (deflated 45%)
    adding: server.key (deflated 23%)
  [root@t2 server_crt]# LS
  -bash: LS: command not found
  [root@t2 server_crt]# LS
  -bash: LS: command not found
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ;s
  -bash: syntax error near unexpected token `;'
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/server
  server/      server.conf  server_crt/  server.crt   server.key   
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/server_crt/
  cp: overwrite ‘/etc/openvpn/server_crt/ca.crt’? y
  cp: overwrite ‘/etc/openvpn/server_crt/server.crt’? y
  cp: overwrite ‘/etc/openvpn/server_crt/server.key’? y
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  ca.key  crl.pem  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd ..
  [root@t2 etc]# ls
  adjtime            cron.weekly              gdbinit.d    issue.net       motd                       polkit-1          rsyslog.conf         sysctl.d
  adjtime.rpmsave    crypttab                 GeoIP.conf   kernel          mtab                       popt.d            rsyslog.d            systemd
  aliases            csh.cshrc                gnupg        krb5.conf       my.cnf                     postfix           rwtab                system-lsb
  aliases.db         csh.login                grafana      krb5.conf.d     my.cnf.d                   ppp               rwtab.d              system-release
  alinux-release     dbus-1                   GREP_COLORS  ld.so.cache     netconfig                  prelink.conf.d    sasl2                telegraf
  alternatives       default                  groff        ld.so.conf      NetworkManager             printcap          securetty            terminfo
  anacrontab         depmod.d                 group        ld.so.conf.d    networks                   profile           security             tmpfiles.d
  anolis-release     dhcp                     group-       libaudit.conf   nfs.conf                   profile.d         selinux              tuned
  asound.conf        DIR_COLORS               grub2.cfg    libnl           nfsmount.conf              protocols         services             udev
  at.deny            DIR_COLORS.256color      grub.d       libreport       nscd.conf                  python            sestatus.conf        update-motd.d
  audisp             DIR_COLORS.lightbgcolor  gshadow      libuser.conf    nsswitch.conf              rc0.d             shadow               vconsole.conf
  audit              docker                   gshadow-     locale.conf     nsswitch.conf.bak          rc1.d             shadow-              vimrc
  bash_completion.d  dracut.conf              gss          localtime       nsswitch.conf.rpmnew       rc2.d             shells               virc
  bashrc             dracut.conf.d            gssproxy     login.defs      ntp.conf                   rc3.d             skel                 wgetrc
  binfmt.d           e2fsck.conf              host.conf    logrotate.conf  oci-register-machine.conf  rc4.d             ssh                  wireguard
  centos-release     environment              hostname     logrotate.d     oci-umount                 rc5.d             ssl                  wpa_supplicant
  chkconfig.d        ethertypes               hosts        lsb-release.d   oci-umount.conf            rc6.d             statetab             X11
  chrony.conf        exports                  hosts.allow  lvm             openldap                   rc.d              statetab.d           xdg
  chrony.keys        exports.d                hosts.deny   machine-id      openvpn                    rc.local          subgid               xinetd.d
  cloud              favicon.png              idmapd.conf  magic           opt                        redhat-release    subuid               yum
  containers         filesystems              image-id     mailcap         os-release                 request-key.conf  sudo.conf            yum.conf
  cron.d             firewalld                influxdb     mail.rc         pam.d                      request-key.d     sudoers              yum.repos.d
  cron.daily         fonts                    init.d       man_db.conf     passwd                     resolv.conf       sudoers.d
  cron.deny          fstab                    inittab      mime.types      passwd-                    resolv.conf.save  sudo-ldap.conf
  cron.hourly        fuse.conf                inputrc      mke2fs.conf     pkcs11                     rpc               sysconfig
  cron.monthly       gcrypt                   iproute2     modprobe.d      pki                        rpm               sysctl.conf
  crontab            gdbinit                  issue        modules-load.d  pm                         rsyncd.conf       sysctl.conf.rpmsave
  [root@t2 etc]# cd openvpn/
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/
  cp: overwrite ‘/etc/openvpn/ca.crt’? y
  cp: overwrite ‘/etc/openvpn/server.crt’? y
  cp: overwrite ‘/etc/openvpn/server.key’? y
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn
  Failed to restart openvpn.service: Unit not found.
  [root@t2 openvpn]# systemctl status openvpn
  Unit openvpn.service could not be found.
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2022-11-25 14:59:33 CST; 3 months 20 days ago
   Main PID: 24698 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─24698 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf
  ^C
  [root@t2 openvpn]# systemctl restart openvpn@server.service
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:25:09 CST; 1s ago
   Main PID: 15869 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─15869 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:25:09 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:09 t2 openvpn[15869]: Fri Mar 17 15:25:09 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:09 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# systemctl stop openvpn@server.service 
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: inactive (dead) since Fri 2023-03-17 15:25:35 CST; 2s ago
    Process: 15869 ExecStart=/usr/sbin/openvpn --cd /etc/openvpn/ --config %i.conf (code=exited, status=0/SUCCESS)
   Main PID: 15869 (code=exited, status=0/SUCCESS)
     Status: "Initialization Sequence Completed"

  Mar 17 15:25:09 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:09 t2 openvpn[15869]: Fri Mar 17 15:25:09 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:09 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  Mar 17 15:25:35 t2 systemd[1]: Stopping OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:35 t2 systemd[1]: Stopped OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# systemctl start openvpn@server.service
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:25:43 CST; 29s ago
   Main PID: 15925 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─15925 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:25:43 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:43 t2 openvpn[15925]: Fri Mar 17 15:25:43 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:43 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# cd /var/lo
  local/ lock/  log/   
  [root@t2 openvpn]# cd /var/log/
  anaconda/ audit/    chrony/   grafana/  influxdb/ journal/  openvpn/  rhsm/     sa/       telegraf/ tuned/    
  [root@t2 openvpn]# cd /var/log/openvpn/
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# tail -f openvpn.log 
  Fri Mar 17 15:26:21 2023 58.33.81.92:49850 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:21 2023 58.33.81.92:63785 Connection reset, restarting [0]
  Fri Mar 17 15:26:21 2023 58.33.81.92:63785 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 TLS: Initial packet from [AF_INET]58.33.81.92:57615, sid=e879fe27 d222dded
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 Connection reset, restarting [0]
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:22 2023 TCP connection established with [AF_INET]101.85.37.188:13366
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 TLS: Initial packet from [AF_INET]101.85.37.188:13366, sid=37c74b71 7dbb2f42
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 Connection reset, restarting [0]
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:37 2023 TCP connection established with [AF_INET]58.33.81.92:49851
  Fri Mar 17 15:26:37 2023 58.33.81.92:49851 TCP connection established with [AF_INET]58.33.81.92:63787
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 TCP connection established with [AF_INET]58.33.81.92:57617
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 TLS: Initial packet from [AF_INET]58.33.81.92:49851, sid=64e00504 c4fb8b9b
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 Connection reset, restarting [0]
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 TLS: Initial packet from [AF_INET]58.33.81.92:63787, sid=0fada2a3 e7c94ebb
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 Connection reset, restarting [0]
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 TLS: Initial packet from [AF_INET]58.33.81.92:57617, sid=76ba2f59 a275c4f9
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 Connection reset, restarting [0]
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:41 2023 TCP connection established with [AF_INET]101.85.37.188:13379
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 TLS: Initial packet from [AF_INET]101.85.37.188:13379, sid=7ec36c11 e1ee83e9
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 Connection reset, restarting [0]
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 SIGUSR1[soft,connection-reset] received, client-instance restarting
  ^C
  [root@t2 openvpn]# sudo reboot
  Connection to 172.17.21.25 closed by remote host.
  Connection to 172.17.21.25 closed.
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  root@172.17.21.25's password: 
  Last login: Fri Mar 17 15:08:33 2023 from 172.17.21.26

  Welcome to Alibaba Cloud Elastic Compute Service !

  [root@t2 ~]# ls
  centos7-vpn.sh  client.tar.gz   epel-release-latest-7.noarch.rpm  influxdb-1.7.6.x86_64.rpm  master.zip  telegraf-1.15.3-1.x86_64.rpm
  client          controlsfx.jar  grafana-7.1.0-1.x86_64.rpm        influxdb-1.8.3.x86_64.rpm  nginx
  [root@t2 ~]# cd /etc/openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# rm -rf server.key ca.key ca.crt dh.pem 
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# rm -rf easy-rsa/
  [root@t2 openvpn]# ;s
  -bash: syntax error near unexpected token `;'
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# cp cert_2023-208/ ./
  cp: omitting directory ‘cert_2023-208/’
  [root@t2 openvpn]# cp cert_2023-208/* ./
  cp: overwrite ‘./crl.pem’? y
  cp: omitting directory ‘cert_2023-208/easy-rsa’
  cp: overwrite ‘./server.crt’? ^C
  [root@t2 openvpn]# cp -y cert_2023-208/* ./
  cp: invalid option -- 'y'
  Try 'cp --help' for more information.
  [root@t2 openvpn]# cp y cert_2023-208/* ./
  cp: cannot stat ‘y’: No such file or directory
  cp: overwrite ‘./ca.crt’? ^C
  [root@t2 openvpn]# ls
  ca.crt  ca.key  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# \cp  cert_2023-208/* ./
  cp: omitting directory ‘cert_2023-208/easy-rsa’
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd ..
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn
  openvpn@                openvpn-client@         openvpn-server@         openvpn@server.service  
  [root@t2 openvpn]# systemctl restart openvpn@
  openvpn@                openvpn@server.service  
  [root@t2 openvpn]# systemctl restart openvpn@server.service 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# sz ca.crt 

  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd /var/log/openvpn/
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# tail -f openvpn.log 
  Fri Mar 17 15:34:10 2023 --pull-filter ignored for --mode server
  Fri Mar 17 15:34:10 2023 WARNING: POTENTIALLY DANGEROUS OPTION --verify-client-cert none|optional (or --client-cert-not-required) may accept clients which do not present a certificate
  Fri Mar 17 15:34:10 2023 OpenVPN 2.4.12 x86_64-redhat-linux-gnu [Fedora EPEL patched] [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Mar 17 2022
  Fri Mar 17 15:34:10 2023 library versions: OpenSSL 1.0.2k-fips  26 Jan 2017, LZO 2.06
  Fri Mar 17 15:34:10 2023 WARNING: using --duplicate-cn and --client-config-dir together is probably not what you want
  Fri Mar 17 15:34:10 2023 WARNING: --ifconfig-pool-persist will not work with --duplicate-cn
  Fri Mar 17 15:34:10 2023 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
  Fri Mar 17 15:34:10 2023 OpenSSL: error:0906D06C:PEM routines:PEM_read_bio:no start line
  Fri Mar 17 15:34:10 2023 Cannot load DH parameters from /etc/openvpn/crl.pem
  Fri Mar 17 15:34:10 2023 Exiting due to fatal error
  c^H^C
  [root@t2 openvpn]# 
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# cd ..
  [root@t2 log]# ls
  anaconda              cron                          grubby_prune_debug  maillog            messages-20230312   secure-20230312   wtmp
  audit                 cron-20230219                 influxdb            maillog-20230219   openvpn             spooler           yum.log
  boot.log              cron-20230226                 journal             maillog-20230226   openvpn-status.log  spooler-20230219  yum.log-20210101
  boot.log-20200711     cron-20230305                 kern                maillog-20230305   rhsm                spooler-20230226  yum.log-20220101
  btmp                  cron-20230312                 kern-20230219       maillog-20230312   sa                  spooler-20230305  yum.log-20230101
  btmp-20230301         ecs_network_optimization.log  kern-20230226       messages           secure              spooler-20230312
  chrony                firewalld                     kern-20230305       messages-20230219  secure-20230219     tallylog
  cloudinit-deploy.log  grafana                       kern-20230312       messages-20230226  secure-20230226     telegraf
  cloud-init.log        grubby                        lastlog             messages-20230305  secure-20230305     tuned
  [root@t2 log]# cd ..
  [root@t2 var]# ls
  adm  cache  db  empty  games  gopher  kerberos  lib  local  lock  log  mail  nis  opt  preserve  run  spool  tmp  yp
  [root@t2 var]# cd /etc/
  alternatives/      dbus-1/            groff/             logrotate.d/       pki/               rc4.d/             ssl/               wireguard/
  audisp/            default/           grub.d/            lsb-release.d/     pm/                rc5.d/             statetab.d/        wpa_supplicant/
  audit/             depmod.d/          gss/               lvm/               polkit-1/          rc6.d/             sudoers.d/         X11/
  bash_completion.d/ dhcp/              gssproxy/          modprobe.d/        popt.d/            rc.d/              sysconfig/         xdg/
  binfmt.d/          docker/            influxdb/          modules-load.d/    postfix/           request-key.d/     sysctl.d/          xinetd.d/
  chkconfig.d/       dracut.conf.d/     init.d/            my.cnf.d/          ppp/               rpm/               systemd/           yum/
  cloud/             exports.d/         iproute2/          NetworkManager/    prelink.conf.d/    rsyslog.d/         system-lsb/        yum.repos.d/
  containers/        firewalld/         .java/             oci-umount/        profile.d/         rwtab.d/           telegraf/          
  cron.d/            fonts/             kernel/            openldap/          python/            sasl2/             terminfo/          
  cron.daily/        gcrypt/            krb5.conf.d/       openvpn/           rc0.d/             security/          tmpfiles.d/        
  cron.hourly/       gdbinit.d/         ld.so.conf.d/      opt/               rc1.d/             selinux/           tuned/             
  cron.monthly/      gnupg/             libnl/             pam.d/             rc2.d/             skel/              udev/              
  cron.weekly/       grafana/           libreport/         pkcs11/            rc3.d/             ssh/               update-motd.d/     
  [root@t2 var]# cd /etc/
  alternatives/      dbus-1/            groff/             logrotate.d/       pki/               rc4.d/             ssl/               wireguard/
  audisp/            default/           grub.d/            lsb-release.d/     pm/                rc5.d/             statetab.d/        wpa_supplicant/
  audit/             depmod.d/          gss/               lvm/               polkit-1/          rc6.d/             sudoers.d/         X11/
  bash_completion.d/ dhcp/              gssproxy/          modprobe.d/        popt.d/            rc.d/              sysconfig/         xdg/
  binfmt.d/          docker/            influxdb/          modules-load.d/    postfix/           request-key.d/     sysctl.d/          xinetd.d/
  chkconfig.d/       dracut.conf.d/     init.d/            my.cnf.d/          ppp/               rpm/               systemd/           yum/
  cloud/             exports.d/         iproute2/          NetworkManager/    prelink.conf.d/    rsyslog.d/         system-lsb/        yum.repos.d/
  containers/        firewalld/         .java/             oci-umount/        profile.d/         rwtab.d/           telegraf/          
  cron.d/            fonts/             kernel/            openldap/          python/            sasl2/             terminfo/          
  cron.daily/        gcrypt/            krb5.conf.d/       openvpn/           rc0.d/             security/          tmpfiles.d/        
  cron.hourly/       gdbinit.d/         ld.so.conf.d/      opt/               rc1.d/             selinux/           tuned/             
  cron.monthly/      gnupg/             libnl/             pam.d/             rc2.d/             skel/              udev/              
  cron.weekly/       grafana/           libreport/         pkcs11/            rc3.d/             ssh/               update-motd.d/     
  [root@t2 var]# cd /etc/openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd private/
  [root@t2 private]# ls
  ca.key  client.key  server.key
  [root@t2 private]# cd ..
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd x509-types/
  [root@t2 x509-types]# ls
  ca  client  code-signing  COMMON  email  kdc  server  serverClient
  [root@t2 x509-types]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa gen-dh

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating DH parameters, 2048 bit long safe prime, generator 2
  This is going to take a long time
  ..........................................................................................................................................................................................................................................................................................................................................................+.......................+...................................+.........................+...............................+.............................................................................................+.....................................................................................................................+........................................................................................................................................+..........................................................+.....................................+.....................................................................................+.................+......................................+...................+.............................................................+...............+..................................................................................................................................+..............................................................................................................................................................................................................................................................................................................................................................................+...............................................+....................................................................+...................................................+.....................................................................................................................................................................................................................................................................................+....+.............................................................+..........................+.............................................................................................................................................................................+...................................+................................................................................+.......................................................................................................................................................+..............................................................................................................................+................................................................................+...+.............................................................+..........................+.................................................................................................................................................................................................................................................................................................................+..........................................................+....+...................................................................................+........................+.......................................................+......................................................................................+..............+...............................................................+..........+..............................................................................................................................................................................................................................................................................................+..........................................................................................................................................................................+..................................................................................................................................................+.................+..............................+..............................................................+......................................................+............................................+.......................................+.....................................................................................................................+.........................................................................+.....................................................+........................................+................................+.............................................................................................................................................................................................................................................................................................................................................................................................................................................................+...........................................................................................................+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................+........................+....................................................................................................................................................................................+....................................+..........+................................................................................................................................+......+.+....+...............................................................+..................................................................................................................................................................+..............+......................................................+..............+........+....+.....................................................+.................................................................................................................................+.......................+....................................................................................................................................................................................................................................................................................+.......+.............................................................................................+..................................................................................+............................................................................+......................................................................+..+..........................................................................................+......................................................................+...........................+.....................................................................................................................................+..............................................................................................................................................+..........................................+.........+..........................+........................................................................................................................................................+...................................................................................................................+...................+.................................................................................................................................................................................................................................................................................+............+..........................+............+...............................................................................................................+...........................................................................................................................................+............................................................................................................................................................................................................................................................................................................................++*++*

  DH parameters of size 2048 created at /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/dh.pem


  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem  index.txt       index.txt.attr.old  issued               private  reqs     safessl-easyrsa.cnf  serial.old
  certs_by_serial  dh.pem   index.txt.attr  index.txt.old       openssl-easyrsa.cnf  renewed  revoked  serial
  [root@t2 pki]# cp dh.pem /etc/openvpn/
  [root@t2 pki]# cd ../../../
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  ipp.txt               psw-file  server.conf  server.crt
  ca.key  cert_2023-208  client       dh.pem   openvpn-password.log  server    server_crt   server.key
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn@server.service 
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:44:05 CST; 8s ago
   Main PID: 2483 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─2483 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:44:05 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:44:05 t2 openvpn[2483]: Fri Mar 17 15:44:05 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:44:05 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]#
  </code></pre>

markdown_text:
  ```bash
  Xshell 7 (Build 0093)
  Copyright (c) 2020 NetSarang Computer, Inc. All rights reserved.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.25:22...
  Could not connect to '172.17.21.25' (port 22): Connection failed.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.25:22...
  Could not connect to '172.17.21.25' (port 22): Connection failed.

  Type `help' to learn how to use Xshell prompt.
  [C:\~]$ 

  Connecting to 172.17.21.26:22...
  Connection established.
  To escape to local shell, press Ctrl+Alt+].

  WARNING! The remote SSH server rejected X11 forwarding request.

  Welcome to Alibaba Cloud Elastic Compute Service !

  Last login: Wed Mar 15 16:39:34 2023 from 172.17.21.25
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  The authenticity of host '172.17.21.25 (172.17.21.25)' can't be established.
  ECDSA key fingerprint is SHA256:VY15Lar9HjcUt2OwYClxK4L2AnLJBAh/GheCVG3iep4.
  Are you sure you want to continue connecting (yes/no/[fingerprint])? 
  Host key verification failed.
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  The authenticity of host '172.17.21.25 (172.17.21.25)' can't be established.
  ECDSA key fingerprint is SHA256:VY15Lar9HjcUt2OwYClxK4L2AnLJBAh/GheCVG3iep4.
  Are you sure you want to continue connecting (yes/no/[fingerprint])? y
  Please type 'yes', 'no' or the fingerprint: y
  Please type 'yes', 'no' or the fingerprint: yes
  Warning: Permanently added '172.17.21.25' (ECDSA) to the list of known hosts.
  root@172.17.21.25's password: 
  Last login: Fri Nov 25 15:00:03 2022 from 192.168.112.30

  Welcome to Alibaba Cloud Elastic Compute Service !

  [root@t2 ~]# ls
  centos7-vpn.sh  client.tar.gz   epel-release-latest-7.noarch.rpm  influxdb-1.7.6.x86_64.rpm  master.zip  telegraf-1.15.3-1.x86_64.rpm
  client          controlsfx.jar  grafana-7.1.0-1.x86_64.rpm        influxdb-1.8.3.x86_64.rpm  nginx
  [root@t2 ~]# cd /opt/
  [root@t2 opt]# ls
  project
  [root@t2 opt]# cd project/
  [root@t2 project]# ls
  db.sqlite3  manage.py  nohup.out  Pipfile  Pipfile.lock  requirements.txt  start.sh  static  templates  vpn  vpn_ms
  [root@t2 project]# cd vpn
  [root@t2 vpn]# ls
  admin.py  apps.py  context_processors.py  __init__.py  migrations  models.py  __pycache__  tests.py  views.py
  [root@t2 vpn]# cd /etc/
  [root@t2 etc]# ls
  adjtime            cron.weekly              gdbinit.d    issue.net       motd                       polkit-1          rsyslog.conf         sysctl.d
  adjtime.rpmsave    crypttab                 GeoIP.conf   kernel          mtab                       popt.d            rsyslog.d            systemd
  aliases            csh.cshrc                gnupg        krb5.conf       my.cnf                     postfix           rwtab                system-lsb
  aliases.db         csh.login                grafana      krb5.conf.d     my.cnf.d                   ppp               rwtab.d              system-release
  alinux-release     dbus-1                   GREP_COLORS  ld.so.cache     netconfig                  prelink.conf.d    sasl2                telegraf
  alternatives       default                  groff        ld.so.conf      NetworkManager             printcap          securetty            terminfo
  anacrontab         depmod.d                 group        ld.so.conf.d    networks                   profile           security             tmpfiles.d
  anolis-release     dhcp                     group-       libaudit.conf   nfs.conf                   profile.d         selinux              tuned
  asound.conf        DIR_COLORS               grub2.cfg    libnl           nfsmount.conf              protocols         services             udev
  at.deny            DIR_COLORS.256color      grub.d       libreport       nscd.conf                  python            sestatus.conf        update-motd.d
  audisp             DIR_COLORS.lightbgcolor  gshadow      libuser.conf    nsswitch.conf              rc0.d             shadow               vconsole.conf
  audit              docker                   gshadow-     locale.conf     nsswitch.conf.bak          rc1.d             shadow-              vimrc
  bash_completion.d  dracut.conf              gss          localtime       nsswitch.conf.rpmnew       rc2.d             shells               virc
  bashrc             dracut.conf.d            gssproxy     login.defs      ntp.conf                   rc3.d             skel                 wgetrc
  binfmt.d           e2fsck.conf              host.conf    logrotate.conf  oci-register-machine.conf  rc4.d             ssh                  wireguard
  centos-release     environment              hostname     logrotate.d     oci-umount                 rc5.d             ssl                  wpa_supplicant
  chkconfig.d        ethertypes               hosts        lsb-release.d   oci-umount.conf            rc6.d             statetab             X11
  chrony.conf        exports                  hosts.allow  lvm             openldap                   rc.d              statetab.d           xdg
  chrony.keys        exports.d                hosts.deny   machine-id      openvpn                    rc.local          subgid               xinetd.d
  cloud              favicon.png              idmapd.conf  magic           opt                        redhat-release    subuid               yum
  containers         filesystems              image-id     mailcap         os-release                 request-key.conf  sudo.conf            yum.conf
  cron.d             firewalld                influxdb     mail.rc         pam.d                      request-key.d     sudoers              yum.repos.d
  cron.daily         fonts                    init.d       man_db.conf     passwd                     resolv.conf       sudoers.d
  cron.deny          fstab                    inittab      mime.types      passwd-                    resolv.conf.save  sudo-ldap.conf
  cron.hourly        fuse.conf                inputrc      mke2fs.conf     pkcs11                     rpc               sysconfig
  cron.monthly       gcrypt                   iproute2     modprobe.d      pki                        rpm               sysctl.conf
  crontab            gdbinit                  issue        modules-load.d  pm                         rsyncd.conf       sysctl.conf.rpmsave
  [root@t2 etc]# cd op
  openldap/ openvpn/  opt/      
  [root@t2 etc]# cd op
  openldap/ openvpn/  opt/      
  [root@t2 etc]# cd openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# openssl x509 -noout -text in server.crt 
  unknown option in
  usage: x509 args
   -inform arg     - input format - default PEM (one of DER, NET or PEM)
   -outform arg    - output format - default PEM (one of DER, NET or PEM)
   -keyform arg    - private key format - default PEM
   -CAform arg     - CA format - default PEM
   -CAkeyform arg  - CA key format - default PEM
   -in arg         - input file - default stdin
   -out arg        - output file - default stdout
   -passin arg     - private key password source
   -serial         - print serial number value
   -subject_hash   - print subject hash value
   -subject_hash_old   - print old-style (MD5) subject hash value
   -issuer_hash    - print issuer hash value
   -issuer_hash_old    - print old-style (MD5) issuer hash value
   -hash           - synonym for -subject_hash
   -subject        - print subject DN
   -issuer         - print issuer DN
   -email          - print email address(es)
   -startdate      - notBefore field
   -enddate        - notAfter field
   -purpose        - print out certificate purposes
   -dates          - both Before and After dates
   -modulus        - print the RSA key modulus
   -pubkey         - output the public key
   -fingerprint    - print the certificate fingerprint
   -alias          - output certificate alias
   -noout          - no certificate output
   -ocspid         - print OCSP hash values for the subject name and public key
   -ocsp_uri       - print OCSP Responder URL(s)
   -trustout       - output a "trusted" certificate
   -clrtrust       - clear all trusted purposes
   -clrreject      - clear all rejected purposes
   -addtrust arg   - trust certificate for a given purpose
   -addreject arg  - reject certificate for a given purpose
   -setalias arg   - set certificate alias
   -days arg       - How long till expiry of a signed certificate - def 30 days
   -checkend arg   - check whether the cert expires in the next arg seconds
                     exit 1 if so, 0 if not
   -signkey arg    - self sign cert with arg
   -x509toreq      - output a certification request object
   -req            - input is a certificate request, sign and output.
   -CA arg         - set the CA certificate, must be PEM format.
   -CAkey arg      - set the CA key, must be PEM format
                     missing, it is assumed to be in the CA file.
   -CAcreateserial - create serial number file if it does not exist
   -CAserial arg   - serial file
   -set_serial     - serial number to use
   -text           - print the certificate in text form
   -C              - print out C code forms
   -<dgst>         - digest to use, see openssl dgst -h output for list
   -extfile        - configuration file with X509V3 extensions to add
   -extensions     - section from config file with X509V3 extensions to add
   -clrext         - delete extensions before signing and input certificate
   -nameopt arg    - various certificate name options
   -engine e       - use engine e, possibly a hardware device.
   -certopt arg    - various certificate text options
   -checkhost host - check certificate matches "host"
   -checkemail email - check certificate matches "email"
   -checkip ipaddr - check certificate matches "ipaddr"
  [root@t2 openvpn]# openssl x509 -noout -text -in server.crt 
  Certificate:
      Data:
          Version: 3 (0x2)
          Serial Number:
              95:a6:bd:d4:02:d2:63:dc:bd:08:39:14:6e:a3:3c:94
      Signature Algorithm: sha256WithRSAEncryption
          Issuer: CN=micvs
          Validity
              Not Before: Dec 12 04:50:26 2020 GMT
              Not After : Mar 17 04:50:26 2023 GMT
          Subject: CN=micvs_server
          Subject Public Key Info:
              Public Key Algorithm: rsaEncryption
                  Public-Key: (2048 bit)
                  Modulus:
                      00:d8:2e:eb:0a:e8:e1:9f:87:b8:79:61:ab:96:75:
                      be:87:8e:33:74:80:d0:0b:aa:8e:7d:56:39:04:e4:
                      89:d4:38:a9:77:fc:c0:60:27:f7:1c:0f:cc:cb:30:
                      2f:c9:92:ed:06:06:c7:b3:5b:9e:fc:c3:f5:ca:8a:
                      d0:8d:ff:28:e4:29:03:41:5b:fd:8b:97:f9:d1:17:
                      2f:ce:37:8e:36:dd:d9:e0:94:7e:a6:16:9f:2e:98:
                      13:13:20:05:fa:3a:1a:17:55:87:0b:68:51:4c:7f:
                      64:32:13:94:5e:31:3a:12:5e:02:33:1e:67:99:a7:
                      0f:ee:2e:a9:5c:0d:d4:31:e4:8b:8f:ea:70:3d:1b:
                      d9:81:5d:2a:5f:6f:1c:d2:89:f9:e7:85:01:ee:6d:
                      ec:64:4e:5d:1b:91:e7:f7:98:eb:24:85:fe:bc:7c:
                      28:3a:2f:8a:ca:bf:50:53:db:66:59:4a:fa:2e:eb:
                      1f:81:f5:64:86:f0:1e:a3:87:52:95:59:6d:39:5d:
                      0b:71:bc:48:70:d2:cc:7f:bd:0c:2e:2d:0e:7a:0b:
                      01:3e:1b:63:3d:d8:af:8c:42:50:be:aa:6e:11:a9:
                      0f:10:71:38:22:95:42:83:d6:02:c3:2d:b2:83:01:
                      11:18:c9:b9:eb:28:72:b6:72:fe:e0:2c:98:0c:01:
                      8a:9d
                  Exponent: 65537 (0x10001)
          X509v3 extensions:
              X509v3 Basic Constraints: 
                  CA:FALSE
              X509v3 Subject Key Identifier: 
                  5C:E3:7F:82:65:F7:D7:A6:09:05:4F:88:09:E9:C2:75:5F:5F:B4:CE
              X509v3 Authority Key Identifier: 
                  keyid:51:93:EB:47:9A:69:76:9B:5D:46:5F:EC:C6:8D:50:2D:18:6D:8D:11
                  DirName:/CN=micvs
                  serial:8F:B4:36:E7:E5:0C:1A:B6

              X509v3 Extended Key Usage: 
                  TLS Web Server Authentication
              X509v3 Key Usage: 
                  Digital Signature, Key Encipherment
              X509v3 Subject Alternative Name: 
                  DNS:micvs_server
      Signature Algorithm: sha256WithRSAEncryption
           64:e1:1c:b9:c4:58:1b:69:93:d6:be:38:39:b1:ca:c1:41:89:
           e4:12:16:b8:b7:66:6a:15:1e:75:41:d9:93:af:d2:ff:d0:78:
           47:0a:cd:3d:de:cf:fc:26:f7:d1:76:6c:fc:b4:9c:78:53:b9:
           02:0a:35:fd:83:78:4e:ef:b4:ba:60:93:b9:64:b6:fb:25:9c:
           2a:53:ec:ec:b4:6d:a5:5e:15:46:c1:c1:6f:2c:99:cf:36:91:
           10:07:05:27:91:ff:20:3e:29:8f:20:b3:58:34:04:b6:d0:b1:
           e9:a3:27:d8:81:77:cf:f0:19:8e:5c:a1:5e:39:d1:a3:ba:e4:
           bf:9e:0d:57:a3:6c:b4:b0:10:81:29:08:d0:55:35:b8:72:b9:
           b4:2a:04:18:99:e2:08:70:6b:1f:fc:ee:2f:94:4c:59:0c:e9:
           66:1a:fa:a5:e6:6f:a2:f3:66:18:22:da:17:a2:a8:76:01:06:
           98:f7:a2:8a:b5:1e:ca:b0:ae:6f:eb:f4:1a:6e:e2:89:cd:87:
           39:67:28:79:f6:f4:86:7d:de:5e:b1:10:3c:a7:72:69:fd:58:
           9d:1e:9f:54:e2:06:3c:a7:39:4c:a5:d0:4b:12:15:8a:c7:95:
           06:41:33:f4:74:0d:68:7c:5e:f5:67:f2:d1:cc:fc:e3:aa:14:
           49:09:5a:7b
  [root@t2 openvpn]# ls
  ca.crt  ccd  checkpsw.sh  client  dh.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt  server.key
  [root@t2 openvpn]# mkdir cert_2023-208
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  [root@t2 cert_2023-208]# cp -r /etc/open
  openldap/ openvpn/  
  [root@t2 cert_2023-208]# cp -r /etc/open
  openldap/ openvpn/  
  [root@t2 cert_2023-208]# cp -r /etc/openvpn/easy-rsa ./
  [root@t2 cert_2023-208]# ls
  easy-rsa
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# ./easyrsa3/
  easyrsa     pki/        x509-types/ 
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# rm -rf pki/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa init-pki

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars

  init-pki complete; you may now create a CA or requests.
  Your newly created PKI dir is: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki


  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa --batch build-ca nopass
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating RSA private key, 2048 bit long modulus
  ...................+++
  .......................................................+++
  e is 65537 (0x10001)

  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopassEASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass^C
  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating a 2048 bit RSA private key
  ............+++
  ................................................................+++
  writing new private key to '/etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-14928.jQN2WT/tmp.EkeT0G'
  -----
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-14928.jQN2WT/tmp.DmWly1
  Check that the request matches the signature
  Signature ok
  The Subject's Distinguished Name is as follows
  commonName            :ASN.1 12:'server'
  Certificate is to be certified until Mar 14 07:15:04 2033 GMT (3650 days)

  Write out database with 1 new entries
  Data Base Updated

  [root@t2 easyrsa3]# EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full client nopass

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating a 2048 bit RSA private key
  ......+++
  .................................+++
  writing new private key to '/etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15028.kmnt4D/tmp.rZUlnF'
  -----
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15028.kmnt4D/tmp.cqwOFp
  Check that the request matches the signature
  Signature ok
  The Subject's Distinguished Name is as follows
  commonName            :ASN.1 12:'client'
  Certificate is to be certified until Mar 14 07:15:19 2033 GMT (3650 days)

  Write out database with 1 new entries
  Data Base Updated

  [root@t2 easyrsa3]# EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Using configuration from /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/easy-rsa-15103.TnYFbh/tmp.RbHEgL

  An updated CRL has been created.
  CRL file: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/crl.pem


  [root@t2 easyrsa3]# cp pki/ca.crt pki/private/ca.key pki/issued/server.crt pki/private/server.key pki/crl.pem ../../
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd ..
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# chown nobody:nobody crl.pem 
  [root@t2 cert_2023-208]#  openssl verify -CAfile ca.crt -purpose sslserver server.crt
  server.crt: OK
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd  client/
  [root@t2 client]# ls
  [root@t2 client]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# l
  -bash: l: command not found
  [root@t2 server]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# ls
  [root@t2 server]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server
  [root@t2 server]# cd ..
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# zip ./* Expires_2023年3月17日.zip
  	zip warning: missing end signature--probably not a zip file (did you
  	zip warning: remember to use binary mode when you transferred it?)
  	zip warning: (if you are trying to read a damaged archive try -F)

  zip error: Zip file structure invalid (./ca.crt)
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  server.crt  server.key
  [root@t2 server_crt]# zip  Expires_2023年3月17日.zip ./*
    adding: ca.crt (deflated 27%)
    adding: dh.pem (deflated 18%)
    adding: server.crt (deflated 45%)
    adding: server.key (deflated 23%)
  [root@t2 server_crt]# LS
  -bash: LS: command not found
  [root@t2 server_crt]# LS
  -bash: LS: command not found
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# ls
  ca.crt  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ;s
  -bash: syntax error near unexpected token `;'
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/server
  server/      server.conf  server_crt/  server.crt   server.key   
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/server_crt/
  cp: overwrite ‘/etc/openvpn/server_crt/ca.crt’? y
  cp: overwrite ‘/etc/openvpn/server_crt/server.crt’? y
  cp: overwrite ‘/etc/openvpn/server_crt/server.key’? y
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd server_crt/
  [root@t2 server_crt]# ls
  ca.crt  ca.key  crl.pem  dh.pem  Expires_2023年3月17日.zip  server.crt  server.key
  [root@t2 server_crt]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd ..
  [root@t2 etc]# ls
  adjtime            cron.weekly              gdbinit.d    issue.net       motd                       polkit-1          rsyslog.conf         sysctl.d
  adjtime.rpmsave    crypttab                 GeoIP.conf   kernel          mtab                       popt.d            rsyslog.d            systemd
  aliases            csh.cshrc                gnupg        krb5.conf       my.cnf                     postfix           rwtab                system-lsb
  aliases.db         csh.login                grafana      krb5.conf.d     my.cnf.d                   ppp               rwtab.d              system-release
  alinux-release     dbus-1                   GREP_COLORS  ld.so.cache     netconfig                  prelink.conf.d    sasl2                telegraf
  alternatives       default                  groff        ld.so.conf      NetworkManager             printcap          securetty            terminfo
  anacrontab         depmod.d                 group        ld.so.conf.d    networks                   profile           security             tmpfiles.d
  anolis-release     dhcp                     group-       libaudit.conf   nfs.conf                   profile.d         selinux              tuned
  asound.conf        DIR_COLORS               grub2.cfg    libnl           nfsmount.conf              protocols         services             udev
  at.deny            DIR_COLORS.256color      grub.d       libreport       nscd.conf                  python            sestatus.conf        update-motd.d
  audisp             DIR_COLORS.lightbgcolor  gshadow      libuser.conf    nsswitch.conf              rc0.d             shadow               vconsole.conf
  audit              docker                   gshadow-     locale.conf     nsswitch.conf.bak          rc1.d             shadow-              vimrc
  bash_completion.d  dracut.conf              gss          localtime       nsswitch.conf.rpmnew       rc2.d             shells               virc
  bashrc             dracut.conf.d            gssproxy     login.defs      ntp.conf                   rc3.d             skel                 wgetrc
  binfmt.d           e2fsck.conf              host.conf    logrotate.conf  oci-register-machine.conf  rc4.d             ssh                  wireguard
  centos-release     environment              hostname     logrotate.d     oci-umount                 rc5.d             ssl                  wpa_supplicant
  chkconfig.d        ethertypes               hosts        lsb-release.d   oci-umount.conf            rc6.d             statetab             X11
  chrony.conf        exports                  hosts.allow  lvm             openldap                   rc.d              statetab.d           xdg
  chrony.keys        exports.d                hosts.deny   machine-id      openvpn                    rc.local          subgid               xinetd.d
  cloud              favicon.png              idmapd.conf  magic           opt                        redhat-release    subuid               yum
  containers         filesystems              image-id     mailcap         os-release                 request-key.conf  sudo.conf            yum.conf
  cron.d             firewalld                influxdb     mail.rc         pam.d                      request-key.d     sudoers              yum.repos.d
  cron.daily         fonts                    init.d       man_db.conf     passwd                     resolv.conf       sudoers.d
  cron.deny          fstab                    inittab      mime.types      passwd-                    resolv.conf.save  sudo-ldap.conf
  cron.hourly        fuse.conf                inputrc      mke2fs.conf     pkcs11                     rpc               sysconfig
  cron.monthly       gcrypt                   iproute2     modprobe.d      pki                        rpm               sysctl.conf
  crontab            gdbinit                  issue        modules-load.d  pm                         rsyncd.conf       sysctl.conf.rpmsave
  [root@t2 etc]# cd openvpn/
  [root@t2 openvpn]# ls
  ca.crt  cert_2023-208  client  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ccd     checkpsw.sh    dh.pem  ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cp ca.crt ca.key crl.pem server.crt server.key /etc/openvpn/
  cp: overwrite ‘/etc/openvpn/ca.crt’? y
  cp: overwrite ‘/etc/openvpn/server.crt’? y
  cp: overwrite ‘/etc/openvpn/server.key’? y
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn
  Failed to restart openvpn.service: Unit not found.
  [root@t2 openvpn]# systemctl status openvpn
  Unit openvpn.service could not be found.
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2022-11-25 14:59:33 CST; 3 months 20 days ago
   Main PID: 24698 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─24698 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf
  ^C
  [root@t2 openvpn]# systemctl restart openvpn@server.service
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:25:09 CST; 1s ago
   Main PID: 15869 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─15869 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:25:09 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:09 t2 openvpn[15869]: Fri Mar 17 15:25:09 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:09 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# systemctl stop openvpn@server.service 
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: inactive (dead) since Fri 2023-03-17 15:25:35 CST; 2s ago
    Process: 15869 ExecStart=/usr/sbin/openvpn --cd /etc/openvpn/ --config %i.conf (code=exited, status=0/SUCCESS)
   Main PID: 15869 (code=exited, status=0/SUCCESS)
     Status: "Initialization Sequence Completed"

  Mar 17 15:25:09 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:09 t2 openvpn[15869]: Fri Mar 17 15:25:09 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:09 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  Mar 17 15:25:35 t2 systemd[1]: Stopping OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:35 t2 systemd[1]: Stopped OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# systemctl start openvpn@server.service
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:25:43 CST; 29s ago
   Main PID: 15925 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─15925 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:25:43 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:25:43 t2 openvpn[15925]: Fri Mar 17 15:25:43 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:25:43 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# cd /var/lo
  local/ lock/  log/   
  [root@t2 openvpn]# cd /var/log/
  anaconda/ audit/    chrony/   grafana/  influxdb/ journal/  openvpn/  rhsm/     sa/       telegraf/ tuned/    
  [root@t2 openvpn]# cd /var/log/openvpn/
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# tail -f openvpn.log 
  Fri Mar 17 15:26:21 2023 58.33.81.92:49850 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:21 2023 58.33.81.92:63785 Connection reset, restarting [0]
  Fri Mar 17 15:26:21 2023 58.33.81.92:63785 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 TLS: Initial packet from [AF_INET]58.33.81.92:57615, sid=e879fe27 d222dded
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 Connection reset, restarting [0]
  Fri Mar 17 15:26:22 2023 58.33.81.92:57615 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:22 2023 TCP connection established with [AF_INET]101.85.37.188:13366
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 TLS: Initial packet from [AF_INET]101.85.37.188:13366, sid=37c74b71 7dbb2f42
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 Connection reset, restarting [0]
  Fri Mar 17 15:26:22 2023 101.85.37.188:13366 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:37 2023 TCP connection established with [AF_INET]58.33.81.92:49851
  Fri Mar 17 15:26:37 2023 58.33.81.92:49851 TCP connection established with [AF_INET]58.33.81.92:63787
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 TCP connection established with [AF_INET]58.33.81.92:57617
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 TLS: Initial packet from [AF_INET]58.33.81.92:49851, sid=64e00504 c4fb8b9b
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 Connection reset, restarting [0]
  Fri Mar 17 15:26:38 2023 58.33.81.92:49851 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 TLS: Initial packet from [AF_INET]58.33.81.92:63787, sid=0fada2a3 e7c94ebb
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 Connection reset, restarting [0]
  Fri Mar 17 15:26:38 2023 58.33.81.92:63787 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 TLS: Initial packet from [AF_INET]58.33.81.92:57617, sid=76ba2f59 a275c4f9
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 Connection reset, restarting [0]
  Fri Mar 17 15:26:39 2023 58.33.81.92:57617 SIGUSR1[soft,connection-reset] received, client-instance restarting
  Fri Mar 17 15:26:41 2023 TCP connection established with [AF_INET]101.85.37.188:13379
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 TLS: Initial packet from [AF_INET]101.85.37.188:13379, sid=7ec36c11 e1ee83e9
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 Connection reset, restarting [0]
  Fri Mar 17 15:26:41 2023 101.85.37.188:13379 SIGUSR1[soft,connection-reset] received, client-instance restarting
  ^C
  [root@t2 openvpn]# sudo reboot
  Connection to 172.17.21.25 closed by remote host.
  Connection to 172.17.21.25 closed.
  [yangchaojie@t3 ~]$ ssh -p 22 root@172.17.21.25
  root@172.17.21.25's password: 
  Last login: Fri Mar 17 15:08:33 2023 from 172.17.21.26

  Welcome to Alibaba Cloud Elastic Compute Service !

  [root@t2 ~]# ls
  centos7-vpn.sh  client.tar.gz   epel-release-latest-7.noarch.rpm  influxdb-1.7.6.x86_64.rpm  master.zip  telegraf-1.15.3-1.x86_64.rpm
  client          controlsfx.jar  grafana-7.1.0-1.x86_64.rpm        influxdb-1.8.3.x86_64.rpm  nginx
  [root@t2 ~]# cd /etc/openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  easy-rsa  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       dh.pem   ipp.txt   psw-file              server.conf  server.crt
  [root@t2 openvpn]# rm -rf server.key ca.key ca.crt dh.pem 
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  easy-rsa  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# rm -rf easy-rsa/
  [root@t2 openvpn]# ;s
  -bash: syntax error near unexpected token `;'
  [root@t2 openvpn]# ls
  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# cp cert_2023-208/ ./
  cp: omitting directory ‘cert_2023-208/’
  [root@t2 openvpn]# cp cert_2023-208/* ./
  cp: overwrite ‘./crl.pem’? y
  cp: omitting directory ‘cert_2023-208/easy-rsa’
  cp: overwrite ‘./server.crt’? ^C
  [root@t2 openvpn]# cp -y cert_2023-208/* ./
  cp: invalid option -- 'y'
  Try 'cp --help' for more information.
  [root@t2 openvpn]# cp y cert_2023-208/* ./
  cp: cannot stat ‘y’: No such file or directory
  cp: overwrite ‘./ca.crt’? ^C
  [root@t2 openvpn]# ls
  ca.crt  ca.key  ccd  cert_2023-208  checkpsw.sh  client  crl.pem  ipp.txt  openvpn-password.log  psw-file  server  server.conf  server_crt  server.crt
  [root@t2 openvpn]# \cp  cert_2023-208/* ./
  cp: omitting directory ‘cert_2023-208/easy-rsa’
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd ..
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd ..
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn
  openvpn@                openvpn-client@         openvpn-server@         openvpn@server.service  
  [root@t2 openvpn]# systemctl restart openvpn@
  openvpn@                openvpn@server.service  
  [root@t2 openvpn]# systemctl restart openvpn@server.service 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# sz ca.crt 

  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd /var/log/openvpn/
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# tail -f openvpn.log 
  Fri Mar 17 15:34:10 2023 --pull-filter ignored for --mode server
  Fri Mar 17 15:34:10 2023 WARNING: POTENTIALLY DANGEROUS OPTION --verify-client-cert none|optional (or --client-cert-not-required) may accept clients which do not present a certificate
  Fri Mar 17 15:34:10 2023 OpenVPN 2.4.12 x86_64-redhat-linux-gnu [Fedora EPEL patched] [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Mar 17 2022
  Fri Mar 17 15:34:10 2023 library versions: OpenSSL 1.0.2k-fips  26 Jan 2017, LZO 2.06
  Fri Mar 17 15:34:10 2023 WARNING: using --duplicate-cn and --client-config-dir together is probably not what you want
  Fri Mar 17 15:34:10 2023 WARNING: --ifconfig-pool-persist will not work with --duplicate-cn
  Fri Mar 17 15:34:10 2023 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts
  Fri Mar 17 15:34:10 2023 OpenSSL: error:0906D06C:PEM routines:PEM_read_bio:no start line
  Fri Mar 17 15:34:10 2023 Cannot load DH parameters from /etc/openvpn/crl.pem
  Fri Mar 17 15:34:10 2023 Exiting due to fatal error
  c^H^C
  [root@t2 openvpn]# 
  [root@t2 openvpn]# ls
  openvpn.log
  [root@t2 openvpn]# cd ..
  [root@t2 log]# ls
  anaconda              cron                          grubby_prune_debug  maillog            messages-20230312   secure-20230312   wtmp
  audit                 cron-20230219                 influxdb            maillog-20230219   openvpn             spooler           yum.log
  boot.log              cron-20230226                 journal             maillog-20230226   openvpn-status.log  spooler-20230219  yum.log-20210101
  boot.log-20200711     cron-20230305                 kern                maillog-20230305   rhsm                spooler-20230226  yum.log-20220101
  btmp                  cron-20230312                 kern-20230219       maillog-20230312   sa                  spooler-20230305  yum.log-20230101
  btmp-20230301         ecs_network_optimization.log  kern-20230226       messages           secure              spooler-20230312
  chrony                firewalld                     kern-20230305       messages-20230219  secure-20230219     tallylog
  cloudinit-deploy.log  grafana                       kern-20230312       messages-20230226  secure-20230226     telegraf
  cloud-init.log        grubby                        lastlog             messages-20230305  secure-20230305     tuned
  [root@t2 log]# cd ..
  [root@t2 var]# ls
  adm  cache  db  empty  games  gopher  kerberos  lib  local  lock  log  mail  nis  opt  preserve  run  spool  tmp  yp
  [root@t2 var]# cd /etc/
  alternatives/      dbus-1/            groff/             logrotate.d/       pki/               rc4.d/             ssl/               wireguard/
  audisp/            default/           grub.d/            lsb-release.d/     pm/                rc5.d/             statetab.d/        wpa_supplicant/
  audit/             depmod.d/          gss/               lvm/               polkit-1/          rc6.d/             sudoers.d/         X11/
  bash_completion.d/ dhcp/              gssproxy/          modprobe.d/        popt.d/            rc.d/              sysconfig/         xdg/
  binfmt.d/          docker/            influxdb/          modules-load.d/    postfix/           request-key.d/     sysctl.d/          xinetd.d/
  chkconfig.d/       dracut.conf.d/     init.d/            my.cnf.d/          ppp/               rpm/               systemd/           yum/
  cloud/             exports.d/         iproute2/          NetworkManager/    prelink.conf.d/    rsyslog.d/         system-lsb/        yum.repos.d/
  containers/        firewalld/         .java/             oci-umount/        profile.d/         rwtab.d/           telegraf/          
  cron.d/            fonts/             kernel/            openldap/          python/            sasl2/             terminfo/          
  cron.daily/        gcrypt/            krb5.conf.d/       openvpn/           rc0.d/             security/          tmpfiles.d/        
  cron.hourly/       gdbinit.d/         ld.so.conf.d/      opt/               rc1.d/             selinux/           tuned/             
  cron.monthly/      gnupg/             libnl/             pam.d/             rc2.d/             skel/              udev/              
  cron.weekly/       grafana/           libreport/         pkcs11/            rc3.d/             ssh/               update-motd.d/     
  [root@t2 var]# cd /etc/
  alternatives/      dbus-1/            groff/             logrotate.d/       pki/               rc4.d/             ssl/               wireguard/
  audisp/            default/           grub.d/            lsb-release.d/     pm/                rc5.d/             statetab.d/        wpa_supplicant/
  audit/             depmod.d/          gss/               lvm/               polkit-1/          rc6.d/             sudoers.d/         X11/
  bash_completion.d/ dhcp/              gssproxy/          modprobe.d/        popt.d/            rc.d/              sysconfig/         xdg/
  binfmt.d/          docker/            influxdb/          modules-load.d/    postfix/           request-key.d/     sysctl.d/          xinetd.d/
  chkconfig.d/       dracut.conf.d/     init.d/            my.cnf.d/          ppp/               rpm/               systemd/           yum/
  cloud/             exports.d/         iproute2/          NetworkManager/    prelink.conf.d/    rsyslog.d/         system-lsb/        yum.repos.d/
  containers/        firewalld/         .java/             oci-umount/        profile.d/         rwtab.d/           telegraf/          
  cron.d/            fonts/             kernel/            openldap/          python/            sasl2/             terminfo/          
  cron.daily/        gcrypt/            krb5.conf.d/       openvpn/           rc0.d/             security/          tmpfiles.d/        
  cron.hourly/       gdbinit.d/         ld.so.conf.d/      opt/               rc1.d/             selinux/           tuned/             
  cron.monthly/      gnupg/             libnl/             pam.d/             rc2.d/             skel/              udev/              
  cron.weekly/       grafana/           libreport/         pkcs11/            rc3.d/             ssh/               update-motd.d/     
  [root@t2 var]# cd /etc/openvpn/
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  openvpn-password.log  server       server_crt  server.key
  ca.key  cert_2023-208  client       ipp.txt  psw-file              server.conf  server.crt
  [root@t2 openvpn]# cd cert_2023-208/
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd easy-rsa/
  [root@t2 easy-rsa]# ls
  build      COPYING.md  doc       KNOWN_ISSUES  op_test.orig  README.md             release-keys  wop_test.sh
  ChangeLog  distro      easyrsa3  Licensing     op_test.sh    README.quickstart.md  wop_test.bat
  [root@t2 easy-rsa]# cd easyrsa3/
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd private/
  [root@t2 private]# ls
  ca.key  client.key  server.key
  [root@t2 private]# cd ..
  [root@t2 pki]# ls
  ca.crt           crl.pem    index.txt.attr      index.txt.old  openssl-easyrsa.cnf  renewed  revoked              serial
  certs_by_serial  index.txt  index.txt.attr.old  issued         private              reqs     safessl-easyrsa.cnf  serial.old
  [root@t2 pki]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd x509-types/
  [root@t2 x509-types]# ls
  ca  client  code-signing  COMMON  email  kdc  server  serverClient
  [root@t2 x509-types]# cd ..
  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# ./easyrsa gen-dh

  Note: using Easy-RSA configuration from: /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/vars
  Using SSL: openssl OpenSSL 1.0.2k-fips  26 Jan 2017
  Generating DH parameters, 2048 bit long safe prime, generator 2
  This is going to take a long time
  ..........................................................................................................................................................................................................................................................................................................................................................+.......................+...................................+.........................+...............................+.............................................................................................+.....................................................................................................................+........................................................................................................................................+..........................................................+.....................................+.....................................................................................+.................+......................................+...................+.............................................................+...............+..................................................................................................................................+..............................................................................................................................................................................................................................................................................................................................................................................+...............................................+....................................................................+...................................................+.....................................................................................................................................................................................................................................................................................+....+.............................................................+..........................+.............................................................................................................................................................................+...................................+................................................................................+.......................................................................................................................................................+..............................................................................................................................+................................................................................+...+.............................................................+..........................+.................................................................................................................................................................................................................................................................................................................+..........................................................+....+...................................................................................+........................+.......................................................+......................................................................................+..............+...............................................................+..........+..............................................................................................................................................................................................................................................................................................+..........................................................................................................................................................................+..................................................................................................................................................+.................+..............................+..............................................................+......................................................+............................................+.......................................+.....................................................................................................................+.........................................................................+.....................................................+........................................+................................+.............................................................................................................................................................................................................................................................................................................................................................................................................................................................+...........................................................................................................+..............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................+........................+....................................................................................................................................................................................+....................................+..........+................................................................................................................................+......+.+....+...............................................................+..................................................................................................................................................................+..............+......................................................+..............+........+....+.....................................................+.................................................................................................................................+.......................+....................................................................................................................................................................................................................................................................................+.......+.............................................................................................+..................................................................................+............................................................................+......................................................................+..+..........................................................................................+......................................................................+...........................+.....................................................................................................................................+..............................................................................................................................................+..........................................+.........+..........................+........................................................................................................................................................+...................................................................................................................+...................+.................................................................................................................................................................................................................................................................................+............+..........................+............+...............................................................................................................+...........................................................................................................................................+............................................................................................................................................................................................................................................................................................................................++*++*

  DH parameters of size 2048 created at /etc/openvpn/cert_2023-208/easy-rsa/easyrsa3/pki/dh.pem


  [root@t2 easyrsa3]# ls
  easyrsa  openssl-easyrsa.cnf  pki  vars  vars.example  x509-types
  [root@t2 easyrsa3]# cd pki/
  [root@t2 pki]# ls
  ca.crt           crl.pem  index.txt       index.txt.attr.old  issued               private  reqs     safessl-easyrsa.cnf  serial.old
  certs_by_serial  dh.pem   index.txt.attr  index.txt.old       openssl-easyrsa.cnf  renewed  revoked  serial
  [root@t2 pki]# cp dh.pem /etc/openvpn/
  [root@t2 pki]# cd ../../../
  [root@t2 cert_2023-208]# ls
  ca.crt  ca.key  crl.pem  easy-rsa  server.crt  server.key
  [root@t2 cert_2023-208]# cd ..
  [root@t2 openvpn]# ls
  ca.crt  ccd            checkpsw.sh  crl.pem  ipp.txt               psw-file  server.conf  server.crt
  ca.key  cert_2023-208  client       dh.pem   openvpn-password.log  server    server_crt   server.key
  [root@t2 openvpn]# vim server.conf 
  [root@t2 openvpn]# systemctl restart openvpn@server.service 
  [root@t2 openvpn]# systemctl status openvpn@server.service 
  ● openvpn@server.service - OpenVPN Robust And Highly Flexible Tunneling Application On server
     Loaded: loaded (/usr/lib/systemd/system/openvpn@.service; enabled; vendor preset: disabled)
     Active: active (running) since Fri 2023-03-17 15:44:05 CST; 8s ago
   Main PID: 2483 (openvpn)
     Status: "Initialization Sequence Completed"
     CGroup: /system.slice/system-openvpn.slice/openvpn@server.service
             └─2483 /usr/sbin/openvpn --cd /etc/openvpn/ --config server.conf

  Mar 17 15:44:05 t2 systemd[1]: Starting OpenVPN Robust And Highly Flexible Tunneling Application On server...
  Mar 17 15:44:05 t2 openvpn[2483]: Fri Mar 17 15:44:05 2023 DEPRECATED OPTION: --client-cert-not-required, use --verify-client-cert instead
  Mar 17 15:44:05 t2 systemd[1]: Started OpenVPN Robust And Highly Flexible Tunneling Application On server.
  [root@t2 openvpn]# 
  ```


