年度归档: <span>2024 年</span>
年度归档: 2024 年

nestjs框架结合typeorm的typescript实践

nestjs框架结合typeorm的typescript实践

最近在学习nestjs框架,相比之前用过的koa2和express有许多不同,koa2结合sequelize之前已经使用过,感觉还是不错的,这次也是学习一下强大的typeorm,其中一些关键操作记录在这里,一遍日后查看。网络上很多帖子已经过时了,对应的很多方法都已经弃用了,所以踩了不少坑。

如果用nestjs开发,那么cli命令一定要用,非常方便。

创建一个module所有文件

nest g res <module name> <dir>

比如nest g res user modules将会在modules目录下创建user文件夹,里面会创建controller、module、service、entities等文件和目录。我们只需进一步完善即可。

entity

这在typeorm中是对数据库表的映射,这里我们以mysql为例,后续所有操作都是基于mysql,比如以下文件

//modules/building/entities/house/entity.ts
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
import { Building } from './building.entity';
import { Renter } from '@/modules/renter/renter.entity';
@Entity({
  name: 'house'
})
export class House {
  @PrimaryColumn()
  id: string;

  @Column()
  rank: number;

  @Column({ nullable: false})
  cname: string;

  @Column({ nullable: true})
  description: string;

  @Column({ nullable: true })
  usage: string;

  @Column({ nullable: true })
  buildingId: string;

  @ManyToOne(()=> Building, (c)=>c.houses)
  building: Building

  @OneToMany(()=> Renter, (c)=>c.house)
  renters: Renter[]
}

PrimaryColumn是定义主键字段的,如果是自增的话,有单独一个定义PrimaryGeneratedColumn

表与表之间的关系可以通过ManyToOneOneToManyManyToMany实现,这里暂时只用到了一对多的关系。上面的表HouseN:1BuildingHouse1:NRenter,就是这样定义的。按照本人的理解,如果这样定义,意味着mysql中也要定义相应的外键,如果数据库中不创建外键,不建立外键约束,也是可以做到的,那就要在createQueryBuilder中手动指定,没法在封装的findAllfindOne等方法中使用relations约束。

findAll中使用order排序


  async findAll() {
    const buildings = await this.buildingRep.find({
      relations:{
        houses:true
      },
      order:{
        rank: 'ASC'
      }
      
    });
    return buildings ;
  }

网上有的帖子是这么写的

find({
	relations:{
		houses:true
	},
	order:'rank ASC'
})

现在已经不行了,目前用的typeorm是10.0.0

关联查询中的count用法

这个在官方文档中未找到,可能是本人疏忽,后面在issue中找到了用法

  async findHouses(id: string){
    const houses = await  this.dataSource
    .getRepository(House)
    .createQueryBuilder("h")
    .loadRelationCountAndMap('h.renters_count', 'h.renters')
    .orderBy("h.rank")
    .addOrderBy("h.id")
    .where("h.buildingId = :id",{id: id})
    .printSql()
    .getMany()
    return houses;
  }

这里使用了loadRelationCountAndMap方法,意思就是相关表先count然后再map到原表字段中,还是比较好理解的,这样返回的数组中每一项都多了一个renters_count字段,就是关联子项的个数,这在一些接口中是很有用的,没必要返回子表的所有字段。

新建实例并插入到表中的优雅做法(官网上是这么说的)

    const newHouse = this.houseRep.create(createHouseDto);
    newHouse.id = randomUUID()
    newHouse.rank = newHouse.rank||99
    await  this.dataSource
    .createQueryBuilder()
    .insert()
    .into(House)
    .values([newHouse])
    .execute()
    return true;
  

这里还是用了dataSource的做法,实际上,中文文档是比较落后的,并没有dataSource的用法,还是在介绍connection的用法,也算是踩了个坑。

image-20240613141644899

看英文官网的介绍:

image-20240613141754229

需要注意的是,nestjs中使用DataSource需要单独设置一下,因为typeorm不限定于具体框架,所以具体实现各个框架略有不同。这里使用方法如下:

  1. 导入相关包
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
  1. 加入到构造器注入中(不知道正确说法)
@Injectable()
export class BuildingService {
  constructor(
    /*@InjectRepository(Building)
    private buildingRep: Repository<Building>,
    @InjectRepository(House)
    private houseRep: Repository<House>,
    */
    @InjectDataSource()
    private dataSource: DataSource,
  ) {}

这里是用在了service中,当然也可以用在controller中,方法一样。只要注入了,下面就可以使用this.DataSource进行连接使用了,具体用法上面已经用过了,参照一下。

dto的继承

便于统一管理,比如createDto中修改了,那么没有必要每一个dto都去修改一遍,用继承完美的实现了自动对应。示例代码如下:

import { PartialType } from '@nestjs/mapped-types';
import { CreateBuildingDto } from './create-building.dto';

export class UpdateBuildingDto extends PartialType(CreateBuildingDto) {}

已建立联系的两个表关联查询

const buildings = await this.buildingRep.find({
      relations:{
        houses:true
      },
      order:{
        rank: 'ASC'
      }
      
    });

未建立关联的两个表进行关联查询

这个需求也是非常大,因为关联表之间需要维护很多东西,在一些场景中,关联不需要非常强,那么直接关联查询将会比较方便,更重要的是,关联表指定返回部分字段。

首先看全部map的方法

    const logs =await this.dataSource
    .getRepository(Log)
    .createQueryBuilder("log")
   .leftJoinAndMapOne('log.renter_info',Renter,'renter','renter.uid=log.renterUid')
    .orderBy("log.createdAt","DESC")
    .limit(2000)
    .getMany()

这样就会将renter表所有字段都map到renter_info字段中

下面这种方法,可以只选择部分字段

    const logs =await this.dataSource
    .getRepository(Log)
    .createQueryBuilder("log")
    .leftJoinAndMapOne('log.renter', Renter,'renter','renter.uid=log.renterUid')
    .select('log')
    .addSelect(['renter.cname','renter.houseId'])
    .orderBy("log.createdAt","DESC")
    .limit(2000)
    .getMany()

上面代码中,select()里面选的是'log',这个一定要加,不然后续选择部分字段将会无效,也可以指定具体的字段,用数组表示即可,比如.select([]'log.id','log.balabala']),下面的addSelect()是针对关联表的字段选择,这里需要注意的是,select('log')必须要加上,否则addSelect将无效,会把所有字段都map进去。

js实现列表转成树的一个有意思的方法

js实现列表转成树的一个有意思的方法

原本以为只完成了一层树,发现这样就能生成整棵树,原因可能与内存中的数据结构有关,没有详细考证,就是感觉很神奇,核心代码其实只有两句

const list2Tree = (list = [])=>{
    if(!list.length){
        return []
    }
    list.forEach(el=>{
        if(list.filter(item => item.pid == el.menuId).length){
            el.children = list.filter(item => item.pid == el.menuId)
        }
    })
    return list.filter(el=>el.menuId == 1)
}

koa2框架获取所有接口列表并通过接口暴露

koa2框架获取所有接口列表并通过接口暴露

router.get('/routesList', async(ctx, next)=>{
//这是唯一需要手动修改的,将路由文件的关键字放到这个数组中,再动态拼接路由文件名,之所以这样处理,是因为这样可以更加灵活地控制返回哪些接口数据
  let routerList = ['menu', 'role', 'user']
  let data = []
  routerList.forEach(el => {
      let filePath = '../routes/' + el + '.route'
      let li = {
        catogory: el,
        list:[]
      }
      li.list.push(...require(filePath).stack.map(i => {
        return {
           methods: i.methods,
           url: i.path
            }   
       }))
       data.push(li)
  });
  ctx.body={
    ...Res[0],
    data:data
  }
})

这样,只要请求 /routesList,便可以得到所有接口的列表,按照路由文件进行了分类,这样便于前端展示,是一个简易版的swagger(只有列表,没有参数信息,没有调试,也没有导出),调试接口的话,完全可以通过apifox等工具实现,又好功能又完善

{
    "status": 0,
    "msg": "成功",
    "data": [
        {
            "catogory": "menu",
            "list": [
                {
                    "methods": [
                        "HEAD",
                        "GET"
                    ],
                    "url": "/menu/getAll"
                },
                {
                    "methods": [
                        "HEAD",
                        "GET"
                    ],
                    "url": "/menu/getMenuTree"
                }
            ]
        },
        {
            "catogory": "role",
            "list": [
                {
                    "methods": [
                        "HEAD",
                        "GET"
                    ],
                    "url": "/role/getAll"
                }
            ]
        }
    ]
}

Koa路由中写一个钩子函数,用于萤石云消息触发钉钉机器人webhook

Koa路由中写一个钩子函数,用于萤石云消息触发钉钉机器人webhook

//钉钉机器人的地址
let webhook = 'https://oapi.dingtalk.com/robot/send?access_token=4dd5cd7xxxxxxxxxxxxxxxxxxxxxxxxxxx'
let whiteList = ['xxxx'] //摄像头序列号白名单,只有在白名单里的才促发报警消息,用于过滤
//获取webhook消息并转发到钉钉机器人
async function sendToDingtalk  (ctx,next) {
    await next()
    ctx.request.body = JSON.parse(ctx.request.body)
    console.log('debugger',ctx.request.body.header)
    console.log('debugger',ctx.request.body)
    let {deviceId, messageTime, channelNo, type, messageId} = ctx.request.body.header
    let {channelName, alarmTime, pictureList } = ctx.request.body.body
    console.log('萤石云消息ID:',messageId)
    let data = null
    if(type=='ys.alarm' &&  whiteList.includes(deviceId) ){
        data =  {
        "msgtype": "markdown",
        "markdown": {
            "title": `【告警消息】${channelName}`, 
            "text": `### 设备名称:${channelName} \n #### 设备序列号:${deviceId} \n #### 通道:${channelNo} \n #### 消息类型:${type} \n #### 时间:${alarmTime} \n ![](${pictureList[0].url})`
            }
        }
    }else if(type=='ys.onoffline'){
        data =  {
        "msgtype": "markdown",
        "markdown": {
            "title": `【上下线消息】${channelName}`, 
            "text": `### 设备名称:${channelName} \n #### 设备序列号:${deviceId} \n #### 通道:${channelNo} \n #### 消息类型:${type} \n #### 时间:${alarmTime}`
            }
        }
    }
    // 判断消息类型,然后发送到钉钉webhook
    if(data != null){
        axios.post(webhook,
           data
        ).then(res=>{
            console.log('钉钉调用成功')
            console.log(res.data)
        }).catch(err=>{
            console.error(err)
        })
    }
    
    ctx.body={
        messageId
    }
}

阿里云ddns ipv6解析程序

阿里云ddns ipv6解析程序

由于网上很多资源没有更新,一些接口阿里云方面已经调整,所以,结合这次自己部署经验,记录如下:
程序没有新增,如果没有记录,请手动添加一条。

# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys

from typing import List
from Tea.core import TeaCore
from alibabacloud_alidns20150109.client import Client as Alidns20150109Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_alidns20150109 import models as alidns_20150109_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
from urllib.request import urlopen

class Sample:
    def __init__(self):
        pass

    @staticmethod
    def create_client(
        access_key_id: str,
        access_key_secret: str,
    ) -> Alidns20150109Client:
        """
        使用AK&SK初始化账号Client
        @param access_key_id:
        @param access_key_secret:
        @return: Client
        @throws Exception
        """
        config = open_api_models.Config(
            # 必填,您的 AccessKey ID,
            access_key_id=access_key_id,
            # 必填,您的 AccessKey Secret,
            access_key_secret=access_key_secret
        )
        # 访问的域名
        config.endpoint = f'alidns.cn-hangzhou.aliyuncs.com'
        return Alidns20150109Client(config)

    @staticmethod
    def main(
        args: List[str],
    ) -> None:
        client = Sample.create_client('key', 'scret') #填写自己的key和密钥,在阿里云申请ram调试账号
        runtime = util_models.RuntimeOptions()
        try:
            ip = urlopen('https://api-ipv6.ip.sb/ip').read()  # 使用IP.SB的接口获取ipv6地址
            ipv6 = str(ip, encoding='utf-8')
            
                    #获取子域名的id信息
            describe_sub_domain_records_request = alidns_20150109_models.DescribeSubDomainRecordsRequest(
                    sub_domain='ipv6.zhangdong.site',
                    type='AAAA'
                ) 
            # 复制代码运行请自行打印 API 的返回值
            resp = client.describe_sub_domain_records_with_options(describe_sub_domain_records_request, runtime)
            # res = UtilClient.to_jsonstring(TeaCore.to_map(resp))
            res = TeaCore.to_map(resp)
            print(res['statusCode'])
            if res['body']['TotalCount'] == 0:
                print("未查到AAAA记录,请新增")
            else:
                Record = res['body']['DomainRecords']['Record'][0]
                if Record['Value'] == ipv6.strip() :
                    print('IPv6地址没变无需更改')
                else:
                     #更新域名信息
                    update_domain_record_request = alidns_20150109_models.UpdateDomainRecordRequest( 
                        rr='ipv6',
                        type='AAAA',
                        value= ipv6,
                        record_id=Record['RecordId'])
                    resp= client.update_domain_record_with_options(update_domain_record_request, runtime)
                    res2 = TeaCore.to_map(resp)
                    if res2['statusCode'] == 200:
                        print("修改域名解析成功")
                    else:
                        print(res2)
        except Exception as error:
            # 如有需要,请打印 error
            UtilClient.assert_as_string(error.message)


if __name__ == '__main__':
    Sample.main(sys.argv[1:])

接着添加一个定时任务即可

特色

webdav服务

webdav服务

使用webdav,可以将网络硬盘加载到本地,用起来非常方便。

webdav服务来自hacdias/webdav,使用go编译的,各端程序都有,用法就是准备一个配置文件,然后启动,如果长久使用,就加入系统服务。

添加服务步骤:

  1. 创建一个webdav.service文件

/etc/systemd/system/下创建webdav.service文件

  1. 编辑文件
[Unit]
Description=WebDAV server
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/webdav --config /xxx/webdav.config
Restart=on-failure

[Install]
WantedBy=multi-user.target
  1. 重新加载

sudo systemctl daemon-reload

  1. 设置开机启动

sudo systemctl enable webdav

附:如果windows加载不了可以使用webdav客户端RailDrive


传图片时出现了一个问题,排查了很长时间,总算解决了。

主要本人的部署还是有些麻烦,转了两道,出问题排查起来比较麻烦

“你没有权限来执行此操作”

原因分析:

一是服务器上面没有写权限,但是除了图片,其它文件都可以上传,因此排除

二是raidrive设置了不能同步文件,因为有的nas系统会有这些限制,排查了一下,raidrive没有这些设置

三是服务器域名监听上有问题,因为本人是通过nginx转发代理的后端服务,因此,这一步容易出现图片类的请求被提前解析了,可能导致失败。因此,本人将配置文件中关于图片的路由全部注释,发现上传图片已经正常了。

这样就可以了

windows docker wsl模式更改镜像存储位置

由于默认位置在C盘,嘎嘎的都下载到C盘,一不小心C盘只剩1MB,紧急扩容。然后再想办法修改docker默认存储位置,通过查找资料,将步骤记录如下:

1.停止docker引擎

2.进入cmd模式

  • 停止服务
wsl --list -v

image-20221213084711712

要确保两个状态都是stopped,上面就是有个还在running,就要查找到相关进程并停止掉。

  • 备份相关文件
    wsl --export docker-desktop-data "E:\\docker-desktop-data.tar"
    
  • wsl取消注册docker-desktop-data,这样C盘下的C:\Users\用户\AppData\Local\Docker\wsl \data\ext4.vhdx将会被自动删除
    wsl --unregister docker-desktop-data
    
  • 设置新的挂载路径,并导入data
    wsl --import docker-desktop-data "E:\\docker\\wsl" "E:\\docker-desktop-data.tar" --version 2
    

结束。

windows11环境下编译安装openpose

windows11环境下编译安装openpose

基本环境

  • windows11
  • cuda11.7
  • cuDNN8.8
  • python3.10.13 (版本应该影响不大)
  • GPU Nvidia 4060

准备物料

models:

链接:https://pan.baidu.com/s/1WLy4sIL4y8kqz5iXBhnhYw?pwd=d34r
提取码:d34r

3rd_party

链接:https://pan.baidu.com/s/1uVJm0eLEGhG5GMUShDkHFw?pwd=nzh5
提取码:nzh5

安装过程

参考博客:OpenPose笔记–Windows+Cmake的pyhton接口编译(CPU_ONLY)_openpose cmake-CSDN博客

  1. 解压3rdparty压缩包,整个替换原有的目录,里面包含了各种依赖文件image-20240528143452881
  2. 解压models压缩包,整个替换image-20240528143655158
  3. 打开编译软件 cmake-guiimage-20240528143946931
  4. 添加python路径变量image-20240528144135770
  5. 选择openpose项目路径以及打包路径image-20240528144221956
  6. 勾选打包选项
    • 使用python的话必选image-20240528144307641
    • cuda支持image-20240528144405833
    • 模型都勾选,方便扩展image-20240528144453957
  7. configure和generateimage-20240528144646007
  8. 用vs 2022打开工程文件,可以直接点击open project按钮
  9. 选择release模式,分别构建右侧四个项目image-20240528144858839
  10. debug模式也可以,选择OpenPoseDemo,然后debug模式启动,默认会调用摄像头image-20240528145157697

    image-20240528145120981

  11. 4个目录都生成成功后,检查一下目录,是否有响应文件生成
    • build_GPU/bin 目录下都是dll文件image-20240528145420254
    • build_GPU/python/openpose/Release 目录下有包文件image-20240528145615509
    • build_GPU/x64/Release 目录下有cuda的openpose动态库image-20240528145747318

编译到此结束,如果以上文件都有,那么编译就成功了。

测试使用

这里主要针对python使用,那么测试python代码导入,打开examples\tutorial_api_python\01_body_from_image.py,将相对路径修改成如下

image-20240528150141573

image-20240528150506387

不知道为什么,系统变量都改了,还是导入失败,那么用替代方案,把build_GPU/bin下面的所有文件、build_GPU/x64/Release下面的openpose.dll都放到build_GPU/python/openpse/Release中,

image-20240528150640767

再次运行,python 01_body_from_image.py,成功!

image-20240528150802775

使用Tensorflow中遇到的问题

首先是GPU版本的安装问题,这个问题比较复杂,既跟python版本有关系,也跟系统平台有关系,最好的解决方法是用conda重新安装

conda create -n tf_gpu tensorflow-gpu

conda会自动处理各种依赖关系,省去了很多烦恼

二是导入tensorflow报错:module ‘numpy’ has no attribute ‘str’.

解决方案是降级: pip install numpy==1.23.4

还有问题是: module ‘tensorflow.keras.layers’ has no attribute ‘Rescaling’

这是由于tensorflow版本的问题,一个api方法写法变了,新版的如下:

layers.experimental.preprocessing.Rescaling

以后怎么改还不知道,当前是1.26.0版本

还有问题: No module named ‘tensorflow.python.trackable’

这个问题可以下载tensorflow源码,里面有trackable文件夹

还有问题:无法导入keras

这个问题也很恶心,参考https://stackoverflow.com/questions/73270410/modulenotfounderror-no-module-named-tensorflow-python-trackable

经过仔细检查和文档校对,有以下几点是安装过程中新弄明白的:

  1. tensorflow在windows下从2.11开始不再支持gpu构建,这个在configure.py文件中有说明

2. gpu版本与cuda版本有对应关系,否则,即便安装了gpu版本的tensorflow,也无法调用gpu,对应关系也并非一一对应,有一个大致范围,英文版文档比中文版文档更加新,里面有写最新的2.16.1,Build from source on Windows  |  TensorFlow (google.cn)

3. windows下用gpu版本的tensorflow,官网说的最简单的办法是用docker,测试下来,其实也很麻烦,首先是安装wsl,然后安装docker-desk,然后再设置绑定wsl,接着要安装wsl版本的cuda以及cudnn,然后再拉取对应版本的tensorflow,然后运行,所有这一切做完了,以为大功告成了。结果还是不支持。应该还是版本问题,但是很明显,这样操作太麻烦了。

我的方案是用tensorflow-gpu==2.10.0,这是最后一个支持gpu的windows单独编译版本,官网上对应的cuda是11.2,实测11.7也是没有问题,cuDNN官网是8.1,实测8.8也是没有问题。最终不懈努力,终于识别了GPU。

4. keras官网上写道:From TensorFlow 2.0 to 2.15,doing pip install tensorflow will also install the corresponding version of Keras2. Tensorflow 2.16,将会自动安装Keras 3.

使用ollama软件实现本地gpt使用

  • 官网下载ollama软件并安装
  • 根据需要设置环境变量,尤其是代理,不然网速非常慢
环境变量

OLLAMA_MODELS F:\xxx 
存放模型目录,如果没有指定,就会默认放C盘,C盘空间大那就随意

OLLAMA_ORIGINS *
来源限制,设置为*表示可以任意源访问,没限制

OLLAMA_HOST 0.0.0.0
访问主机限制,如果想要网络上任意主机访问的话,就设置0.0.0.0

set HTTP_PROXY=http://127.0.0.1:xxxx

  • 运行 ollama run llama2:2b,首次将自动下载模型
  • 下载安装chatbox并添加ollama服务地址 127.0.0.1:11434

大工告成!体验GPT吧!