2024年8月

HarmonyOS SDK实况窗服务
(Live View Kit)作为一个实时呈现应用服务信息变化的小窗口,遍布于设备的各个使用界面,它的魅力在于将复杂的应用场景信息简洁提炼并实时刷新,在不影响当前其他应用操作的情况下,时刻向用户展示最新的信息动态,用户也可以点击实况窗卡片或胶囊进入应用落地页查看详细信息,享受来自应用的高效信息同步服务。

image

实况窗服务为不同场景定制了多样化的卡片模板,包括进度可视化模板、强调文本模板、左右文本模板、赛事比分模板、导航模板,除了这5种卡片形态的模板外,实况窗还有实况胶囊和实况计时器两种形态。下面,本文将详细展示这些模板,介绍其适用的场景,并讲解模板的具体实现步骤。

开发准备

在创建本地实况窗之前,需要先完成基本的准备工作,并开通实况窗服务权益。开通实况窗权益大致分为5个步骤,详细的申请步骤可参考实况窗服务的
开发指南

开发步骤

下面将以在本地创建、更新和结束实况窗为例,展示具体的开发步骤。

1.导入liveViewManager。

在创建本地实况窗前,需要在项目中导入liveViewManager,并新建实况窗控制类,构造isLiveViewEnabled()方法,用于校验实况窗开关是否打开。打开实况窗开关是创建实况窗的前提条件。示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit';

export class LiveViewController {
private static async isLiveViewEnabled(): Promise<boolean> {
return await liveViewManager.isLiveViewEnabled();
  }
}

2.创建实况窗。

实况窗根据扩展区不同共有5种样式模板:进度可视化模板、强调文本模板、左右文本模板、赛事比分模板和导航模板。

进度可视化模板

进度可视化模板可适用于打车、外卖等需要呈现完整进程及当前节点的场景,通过进度可视化模板的实况窗,用户可一眼查看应用的服务进程和实时变化。这里以即时配送场景为例,展示具体的示例代码。

image

在构建LiveViewController后,需要在代码中初始化LiveViewController并调用liveViewManager.startLiveView()方法创建实况窗。其中event的取值为DELIVERY则代表即时配送场景,若取值为TAXI则表示出行打车场景。

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "DELIVERY", // 实况窗的应用场景。DELIVERY:即时配送(外卖、生鲜)
      liveViewData: {
        primary: {
          title: "骑手已接单",
          content: [
            { text: "距商家 " },
            { text: "300 ", textColor: "#FF007DFF" },
            { text: "米 | " },
            { text: "3 ", textColor: "#FF007DFF" },
            { text: "分钟到店" }
          ], // 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 40,
            color: "#FF317AF7",
            backgroundColor: "#f7819ae0",
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UP,
            indicatorIcon: "indicator.png", // 进度条指示器图标,取值为
"/resources/rawfile"路径下的文件名
            lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
            nodeIcons: ["icon_1.png", "icon_2.png", "icon_3.png"] // 进度条每个节点图标,
取值为"/resources/rawfile"路径下的文件名
          }
        }
      }
    };
  }

  private static async isLiveViewEnabled(): Promise<boolean> {
    return await liveViewManager.isLiveViewEnabled();
  }

  private static async buildWantAgent(): Promise<Want> {
    const wantAgentInfo: wantAgent.WantAgentInfo = {
      wants: [
        {
          bundleName: 'xxx.xxx.xxx', // 应用实际bundleName
          abilityName: 'EntryAbility'
        } as Want
      ],
      operationType: wantAgent.OperationType.START_ABILITIES,
      requestCode: 0,
      wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
    };
    const agent = await wantAgent.getWantAgent(wantAgentInfo);
    return agent;
  }
}

强调文本模板

强调文本模板适用于取餐、排队等需要强调部分文本信息的场景。通过强调文本模板实况窗,用户可以快速获取取餐码、排号情况等重要信息,这里以取餐场景为例,展示具体的示例代码。

image

在强调文本模板中,event取值为PICK_UP则代表取餐场景,若取值为QUEUE则代表排队场景。

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "PICK_UP", // 实况窗的应用场景。PICK_UP:取餐。
      liveViewData: {
        primary: {
          title: "餐品已备好",
          content: [
            { text: "请前往", textColor: "#FF000000" },
            { text: "XXX店取餐", textColor: "#FF000000" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            title: "取餐码",
            content: "72988",
            underlineColor: "#FF0A59F7",
            descPic: "coffee.png"
          }
        }
      }
    };
  }
  ... ...
}

左右文本模板

左右文本模板适用于高铁、航班等左右信息对称的场景,通过该模板,用户可以快速获取始发地、目的地、开始和结束时间等出行信息。这里以高铁列车票场景为例,展示具体的示例代码。

image

在左右文本模板中,event取值为TRAIN则代表高铁/火车场景,若取值为FLIGHT则代表航班场景。

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "TRAIN", // 实况窗的应用场景。TRAIN:高铁/火车。
      liveViewData: {
        primary: {
          title: "列车检票提醒",
          content: [
            { text: "检票口 " },
            { text: "6B ", textColor: "#FF007DFF" },
            { text: "| 座位 " },
            { text: "03车 12F", textColor: "#FF007DFF" }
          ],// 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(), // 点击实况窗默认动作。
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_FLIGHT,
            firstTitle: "09:00",
            firstContent: "上海虹桥",
            lastTitle: "14:20",
            lastContent: "汉口",
            spaceIcon: "icon.png",
            isHorizontalLineDisplayed: true,
            additionalText: "以上信息仅供参考" // 扩展区底部内容,仅可用于左右文本模板。
          }
        }
      }
    };
  }
  ... ...
}

赛事比分模板

赛事比分模板适用于竞技比赛的场景,通过该模板,用户可以快速获取比赛队伍、当前比分、场次等比赛信息。

image

在赛事比分模板中,SCORE代表赛事比分场景。

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "SCORE", // 实况窗的应用场景。SCORE:赛事比分。
      liveViewData: {
        primary: {
          title: "第四节比赛中",
          content: [
            { text: "XX VS XX" },
            { text: " | ", textColor: "#f7b7b1b3"},
            { text: "小组赛第五场"}
          ],
          keepTime: 1,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_SCORE,
            hostName: "队名A",
            hostIcon: "host.png",
            hostScore: "110",
            guestName: "队名B",
            guestIcon: "guest.png",
            guestScore: "102",
            competitionDesc: [
              { text: "●", textColor: "#FFFF0000" },
              { text: "Q4" }
            ],
            competitionTime: "02:16",
            isHorizontalLineDisplayed: true
          }
        }
      }
    };
  }
  ... ...
}

导航模板

导航模板适用于出行导航场景。通过该模板,用户可以快速获取所需导航的目的地大致方位信息。在导航模板中,event取值为NAVIGATION则代表导航场景。

image

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "NAVIGATION", // 实况窗的应用场景。NAVIGATION:导航。
      liveViewData: {
        primary: {
          title: "178米后左转",
          content: [
            { text: "去往", textColor: "#FF000000" },
            { text: " 南京东路", textColor: "#FF000000" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_NAVIGATION,
            currentNavigationIcon: "navigation.png",
            navigationIcons: ["left.png","straight.png","straight.png","right.png"]
          }
        }
      }
    };
  }
  ... ...
}

实况胶囊

实况胶囊是在设备熄屏和状态栏中展示的区别于卡片态的另一种实况形态,胶囊内需显示最精简、最重要的内容,保证用户一瞥即得重要信息。并且,胶囊形态各模板参数固定,与创建实况窗时的模板类型无关。

image

在同步创建实况窗胶囊时,需要在liveViewManager.LiveView结构体中携带胶囊所需的参数capsule,不同胶囊类型携带不同的参数。可创建的胶囊类型有:文本胶囊、计时器胶囊和进度胶囊。这里以文本胶囊为例,展示具体的示例代码。

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "DELIVERY", // 实况窗的应用场景。DELIVERY:即时配送(外卖、生鲜)。
      liveViewData: {
        primary: {
          title: "餐品待支付",
          content: [
            { text: "咖啡 ", textColor: "#FF000000" },
            { text: "等2件商品", textColor: "#FF000000" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            title: "待支付金额",
            content: "25.5元",
            underlineColor: "#FF0A59F7",
            descPic: "coffee.png"
          }
        },
        // 实况胶囊相关参数
        capsule: {
          type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
          status: 1,
          icon: "capsule_store.png",
          backgroundColor: "#ff0676e7",
          title: "待支付"
        }
      }
    };
  }
  ... ...
}

实况窗计时器

实况窗计时器适用于排队、抢票等场景。开发者若需要使用实况窗计时器,则需在liveViewManager.LiveView结构体中的配置timer字段,并在当前支持的字段中使用占位符:${placeholder.timer}。

image

具体的示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "QUEUE", // 实况窗的应用场景。QUEUE:排队
      timer: {
        time: 620000,
        isCountdown: false,
        isPaused: false
      },
      liveViewData: {
        primary: {
          title: "大桌4人等位  32桌",
          content: [
            { text: "已等待 " }, 
            { text: "${placeholder.timer}", textColor:"#ff10c1f7" },
            { text: " | 预计还需>30分钟" }
          ], // 所有文本仅能设置为一种颜色,不设置textColor时,默认展示#FF000000
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
            progress: 0,
            color: "#FFFF0000",
            backgroundColor: "#FF000000",
            indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_OVERLAY,
            indicatorIcon: "indicator.png", // 进度条指示器图标,取值为
"/resources/rawfile"路径下的文件名
            lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
            nodeIcons: ["icon_1.png","icon_2.png"] // 进度条每个节点图标,取值为
"/resources/rawfile"路径下的文件名
          }
        }
      }
    };
  }
  ... ...
}

3.本地更新和结束实况窗。

在本地创建完实况窗后,若应用业务状态发生变化,则需要调用liveViewManager的updateLiveView()更新实况窗,更新时对请求体中需要修改的对应参数进行修改。在该应用的服务进程结束时,需要调用stopLiveView()来结束实况窗。这里以即时配送场景的进度可视化模板为例,来说明更新和结束实况窗及实况胶囊的方法,具体示例代码如下:

import { liveViewManager } from '@kit.LiveViewKit';
import { Want, wantAgent } from '@kit.AbilityKit';

export class LiveViewController {
  private static contentColor: string = '#FF000000';
  private static capsuleColor: string = '#FF308977';

  public async startLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 创建实况窗
    const defaultView = await LiveViewController.buildDefaultView();
    return await liveViewManager.startLiveView(defaultView);
  }

  public async updateLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 修改实况窗内容
    const defaultView = await LiveViewController.buildDefaultView();
    defaultView.liveViewData.primary.title = "预计23:49送达";
    defaultView.liveViewData.primary.content = [
      { text: "等待商家接单,",
        textColor: LiveViewController.contentColor },
      { text: "03:20未接单自动取消",
        textColor: LiveViewController.contentColor }
    ];
    defaultView.liveViewData.primary.layoutData = {
      layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
      progress: 0,
      lineType: 0,
      nodeIcons: [ // 进度条每个节点图标,取值为"/resources/rawfile"路径下的文件名
        'icon_store_white.png',
        'icon_finish.png'
      ]
    };
    defaultView.liveViewData.capsule = {
      type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
      status: 1,
      icon: 'capsule_store.png',
      backgroundColor: LiveViewController.capsuleColor,
      title: "待接单"
    };
    // 更新实况窗
    return await liveViewManager.updateLiveView(defaultView);
  }

  public async stopLiveView(): Promise<liveViewManager.LiveViewResult> {
    // 校验实况窗开关是否打开
    if (!LiveViewController.isLiveViewEnabled()) {
      throw new Error("Live view is disabled.");
    }
    // 修改实况窗内容
    const defaultView = await LiveViewController.buildDefaultView();
    defaultView.liveViewData.primary.title = '商品已送达';
    defaultView.liveViewData.primary.content = [
      { text: '感谢您的认可,',
        textColor: LiveViewController.contentColor },
      { text: '期待下一次光临',
        textColor: LiveViewController.contentColor }
    ];
    defaultView.liveViewData.primary.layoutData = {
      layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
      progress: 100,
      lineType: 0,
      nodeIcons: [ // 进度条每个节点图标,取值为"/resources/rawfile"路径下的文件名
        'icon_order.png',
        'icon_finish.png'
      ]
    };
    defaultView.liveViewData.capsule = {
      type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
      status: 1,
      icon: 'capsule_gps.png',
      backgroundColor: LiveViewController.capsuleColor,
      title: '已送达'
    };
    // 结束实况窗
    return await liveViewManager.stopLiveView(defaultView);
  }

  private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
    return {
      // 构造实况窗请求体
      id: 0, // 实况窗ID,开发者生成。
      event: "DELIVERY", // 实况窗的应用场景。DELIVERY:即时配送(外卖、生鲜)
      liveViewData: {
        primary: {
          title: "餐品待支付",
          content: [
            { text: "咖啡 ", textColor: "#FF000000" },
            { text: "等2件商品", textColor: "#FF000000" }
          ],
          keepTime: 15,
          clickAction: await LiveViewController.buildWantAgent(),
          layoutData: {
            layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
            title: "待支付金额",
            content: "25.5元",
            underlineColor: "#FF0A59F7",
            descPic: "coffee.png"
          }
        },
        // 实况胶囊相关参数
        capsule: {
          type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
          status: 1,
          icon: "capsule_store.png",
          backgroundColor: "#FF308977",
          title: "待支付",
          content: "..."
        }
      }
    };
  }
  ... ...
}

了解更多详情>>

获取
实况窗服务开发指导文档

科学计算当中会用到不少浮点数的操作,这些浮点数可能是16位,32位,64位,80位甚至是128位。开源项目SoftFloat提供了一个高效的浮点运算实现,可以在没有硬件支持的情况下,高效模拟浮点数的各种操作。

那么,浮点数之间的比较,基本运算这些究竟是怎么实现的呢,可以拿32位浮点数作为例子。

这是32位浮点数加法的实现,首先声明了一个结构体float32_t。

typedef struct { uint32_t v; } float32_t;

这提供了32位浮点数的底层位表示,同时还声明了一个union。

union ui32_f32 { uint32_t ui; float32_t f; };

一方面保存了浮点数的位表示,另一方面也可以转换为32位无符号整型直接进行比较,这在后面的算法当中会直接涉及。先看看加法。

float32_t f32_add( float32_t a, float32_t b )
{
    union ui32_f32 uA;
    uint_fast32_t uiA;
    union ui32_f32 uB;
    uint_fast32_t uiB;
#if ! defined INLINE_LEVEL || (INLINE_LEVEL < 1)
    float32_t (*magsFuncPtr)( uint_fast32_t, uint_fast32_t );
#endif

    uA.f = a;
    uiA = uA.ui;
    uB.f = b;
    uiB = uB.ui;
#if defined INLINE_LEVEL && (1 <= INLINE_LEVEL)
    if ( signF32UI( uiA ^ uiB ) ) {
        return softfloat_subMagsF32( uiA, uiB );
    } else {
        return softfloat_addMagsF32( uiA, uiB );
    }
#else
    magsFuncPtr =
        signF32UI( uiA ^ uiB ) ? softfloat_subMagsF32 : softfloat_addMagsF32;
    return (*magsFuncPtr)( uiA, uiB );
#endif

}

这里uiA和uiB是存储无符号整型的,signF32UI是提取符号位的。signF32UI(uiA ^ uiB)判断符号位是否相同,如果相同则调用加法,如果符号位不相同则调用减法,因为没有浮点数,所以只能通过整型去模拟,另外,union存储浮点和整型有一个名词,似乎叫类型双关技术?不过这里union存储的只是位表示,并不是真的浮点数。

float32_t f32_sub( float32_t a, float32_t b )
{
    union ui32_f32 uA;
    uint_fast32_t uiA;
    union ui32_f32 uB;
    uint_fast32_t uiB;
#if ! defined INLINE_LEVEL || (INLINE_LEVEL < 1)
    float32_t (*magsFuncPtr)( uint_fast32_t, uint_fast32_t );
#endif

    uA.f = a;
    uiA = uA.ui;
    uB.f = b;
    uiB = uB.ui;
#if defined INLINE_LEVEL && (1 <= INLINE_LEVEL)
    if ( signF32UI( uiA ^ uiB ) ) {
        return softfloat_addMagsF32( uiA, uiB );
    } else {
        return softfloat_subMagsF32( uiA, uiB );
    }
#else
    magsFuncPtr =
        signF32UI( uiA ^ uiB ) ? softfloat_addMagsF32 : softfloat_subMagsF32;
    return (*magsFuncPtr)( uiA, uiB );
#endif

}

减法则是在判断符号那里反过来,其它一样。这时候可以看看比较运算怎么做。

bool f32_le( float32_t a, float32_t b )
{
    union ui32_f32 uA;
    uint_fast32_t uiA;
    union ui32_f32 uB;
    uint_fast32_t uiB;
    bool signA, signB;

    uA.f = a;
    uiA = uA.ui;
    uB.f = b;
    uiB = uB.ui;
    if ( isNaNF32UI( uiA ) || isNaNF32UI( uiB ) ) {
        softfloat_raiseFlags( softfloat_flag_invalid );
        return false;
    }
    signA = signF32UI( uiA );
    signB = signF32UI( uiB );
    return
        (signA != signB) ? signA || ! (uint32_t) ((uiA | uiB)<<1)
            : (uiA == uiB) || (signA ^ (uiA < uiB));

}

最后的表达式有点绕,一步一步拆分。首先符号不相等(一正一负)的话,如果A的符号是1,也就是负数,肯定比B小,否则走 || 后的分支。把A和B的最高位(符号位)剔除,判断是否相同,也就是+0和-0的情况,这里记得别漏了前面的!符号,因为判断两者是否都为0;如果A和B同号的话,如果都是正数则直接比较,如果都是负数,则前面的signA会对结果取反。

结语

最近处于校招阶段,正在准备,有时间会分享自己的心得和体会,希望尽早上岸。

一 、 概述

为了弥补代码的遗失,木舟IOT平台正在加班加点进行研发,后面不只是针对于IOT设备接入上报,告警,视频管理,组态数据可视化大屏,后面还会有快速搭建微服务平台,利用surging.cli工具根据数据库表生成微服务,中间服务,能让程序员快速完成BOSS交给的任务,从而在这个内卷的社会能占有一席之地。这些都是没有完成任务的空话,现在发此篇的目的是作者有能力开发出优秀的IOT平台,先介绍一个比较突出的功能,就是可以基于共享或者独立配置添加网络组件, 下面来介绍一下如何添加网络组件。

一键运行打包成品下载:
https://pan.baidu.com/s/11hcf9ieCkJxlGrzvIuxeQA?pwd=ajsr

测试用户:fanly

测试密码:123456

为了让大家节约时间,能尽快运行产品看到效果,上面有 一键运行打包成品可以进行下载测试运行。

二、如何测试运行

以下是目录结构,

IDE:consul 注册中心

kayak.client: 网关

kayak.server:微服务

apache-skywalking-apm:skywalking链路跟踪

以上是目录结构,大家不需要一个个运行,只需要打开运行startup.bat,如果需要测试skywalking ,只需要apache-skywalking-apm\bin\startup.bat  文件就可以了,以下是运行的界面

三、如何添加组件

1.添加http服务组件,

打开平台界面,然后点击设备接入->网络组件,然后可以看到如下界面

再点击新增组件或者编辑组件,完成后注意启动状态是关闭状态,此时并不能对于该组件功能进行访问调用,只有把启动状态打开,才能访问调用

以上是http服务组件,启动完成后,如果设置了webservice和swagger,你可以访问webservice和swagger,看是否可以访问

2.添加/编辑Tcp服务组件

当添加/编辑Tcp组件时,设置Host:127.0.0.1 ,port:248并且还有解析方式选项,选项里面有不处理,固定长度,分隔符,自定义脚本,下面我们就来看自定义脚本

添加脚本如下:

parser.Fixed(4).Handler(function(buffer){var buf = BytesUtils.Slice(buffer,1,4);
parser.Fixed(buffer.ReadableBytes).Result(buf);
}).Handler(
function(buffer){parser.Fixed(8).Result(buffer);}
).Handler(
function(buffer){
parser.Result(
'处理完成','gb2312').Complete();
}
)

而基于TCP服务代码如下,需要继承于TcpBehavior

internal class TcpDeviceDataService : TcpBehavior, ITcpDeviceDataService
{
private readonly IDeviceProvider _deviceProvider;
public TcpDeviceDataService(IDeviceProvider deviceProvider)
{
_deviceProvider
=deviceProvider;
}

public override
voidLoad(string clientId, NetworkProperties tcpServerProperties)
{
var deviceStatus =_deviceProvider.IsConnected(clientId);this.Parser.HandlePayload().Subscribe(async buffer =>await ParserBuffer(buffer));
}

public override
voidDeviceStatusProcess(DeviceStatus status, string clientId, NetworkProperties tcpServerProperties)
{
//throw new NotImplementedException(); }

public async Task ParserBuffer(IByteBuffer buffer)
{
List
<string> result = new List<string>();while (buffer.ReadableBytes > 0)
{
result.Add(buffer.ReadString(
this.Parser.GetNextFixedRecordLength(),
Encoding.GetEncoding(
"gb2312")));
}
//var str= buffer.ReadString(buffer.ReadableBytes, Encoding.UTF8); var byteBuffer =Unpooled.Buffer();
byteBuffer.WriteString(
"\r\n", Encoding.UTF8);
byteBuffer.WriteString(
"处理完成", Encoding.GetEncoding("gb2312"));
await Sender.SendAndFlushAsync(byteBuffer);
//await Sender.SendAndFlushAsync("消息已接收",Encoding.GetEncoding("gb2312")); this.Parser.Close();
}

public Task
<bool>ChangeDeviceStage(string deviceId)
{
throw newNotImplementedException();
}
}

用测试Tcp调试工具结果如下

3.添加/编辑UDP服务组件

当添加/编辑UDP组件时, 设置Host:127.0.0.1 ,port:267 并且可以是否开启组播

而基于udp服务代码如下,需要继承于
UdpBehavior

internal class UdpDeviceDataService : UdpBehavior, IUdpDeviceDataService
{
public Task
<bool>ChangeDeviceStage(string deviceId)
{
throw newNotImplementedException();
}

public override async Task Dispatch(IEnumerable
<byte>bytes)
{
await Sender.SendAndFlushAsync(
"\r\n", Encoding.UTF8);
await Sender.SendAndFlushAsync(
"处理完成", Encoding.GetEncoding("gb2312"));
}
}

测试结果如下:

4.添加/编辑WebSocket服务组件

当添加/编辑WebSocket组件时, 设置Host:127.0.0.1 ,port:55

而基于websocket服务代码如下,需要继承于WSBehavior

internal classWSDeviceDataService : WSBehavior, IWSDeviceDataService
{
protected override voidOnMessage(MessageEventArgs e)
{
this.Client.Value.SendTo($"send:{e.Data},\r\n reply:hello,welcome to you!",ID);
}
protected override voidOnOpen()
{

}
}

测试结果如下:

5.添加/编辑UDP服务组件

当添加/编辑WebSocket组件时, 设置Host:127.0.0.1 ,port:345

添加greet.proto文件,脚本如下:

syntax = "proto3";

package Greet;

service Greeter {
//Sends a greeting rpc ChangeDeviceStage (DeviceRequest) returns (DeviceReply) {}
}

message DeviceRequest {
string deviceId = 1;
}

message DeviceReply {
bool message = 1;
}

然后再创建GreeterBehavior,继承Greeter.GreeterBase, IServiceBehavior,代码如下

public partial classGreeterBehavior : Greeter.GreeterBase, IServiceBehavior
{
privateServerReceivedDelegate received;public eventServerReceivedDelegate Received
{
add
{
if (value == null)
{
received
+=value;
}
}
remove
{
received
-=value;
}
}
public string MessageId { get; } = Guid.NewGuid().ToString("N");public async Task Write(object result, int statusCode = 200, string exceptionMessage = "")
{
if (received == null)return;var message = new TransportMessage(MessageId, newReactiveResultMessage
{
ExceptionMessage
=exceptionMessage,
StatusCode
=statusCode,
Result
=result

});
awaitreceived(message);
}
public T CreateProxy<T>(string key) where T : class{return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy<T>(key);
}
public objectCreateProxy(Type type)
{
return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy(type);
}
public object CreateProxy(stringkey, Type type)
{
return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy(key, type);
}
public T CreateProxy<T>() where T : class{return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy<T>();
}
public T GetService<T>(string key) where T : class{if (ServiceLocator.Current.IsRegisteredWithKey<T>(key))return ServiceLocator.GetService<T>(key);else return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy<T>(key);
}
public T GetService<T>() where T : class{if (ServiceLocator.Current.IsRegistered<T>())return ServiceLocator.GetService<T>();else return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy<T>();

}
public objectGetService(Type type)
{
if(ServiceLocator.Current.IsRegistered(type))returnServiceLocator.GetService(type);else return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy(type);
}
public object GetService(stringkey, Type type)
{
if(ServiceLocator.Current.IsRegisteredWithKey(key, type))returnServiceLocator.GetService(key, type);else return ServiceLocator.GetService<IServiceProxyFactory>().CreateProxy(key, type);

}
public voidPublish(IntegrationEvent @event)
{
GetService
<IEventBus>().Publish(@event);
}

}

而基于grpc服务代码如下,需要继承于刚刚创建的GreeterBehavior

    public classGrpcDeviceDataService : GreeterBehavior, IGrpcDeviceDataService
{
public override Task<DeviceReply>ChangeDeviceStage(DeviceRequest request, ServerCallContext context)
{
return Task.FromResult(newDeviceReply
{
Message
= true}) ;
}
}

以下是测试结果:

6.添加/编辑MQTT服务组件

当添加/编辑MQTT组件时, 设置Host:127.0.0.1 ,port:425

而基于mqtt服务代码如下,需要继承于MqttBehavior

 public classMQTTDeviceDataService : MqttBehavior, IMQTTDeviceDataService
{
public override async Task<bool> Authorized(string username, stringpassword)
{
bool result = false;if (username == "admin" && password == "123456")
result
= true;return awaitTask.FromResult(result);
}
public async Task<bool> IsOnline(stringdeviceId)
{
return await base.GetDeviceIsOnine(deviceId);
}
public async Task Publish(stringdeviceId, WillMessage message)
{
var willMessage = newMqttWillMessage
{
WillMessage
=message.Message,
Qos
=message.Qos,
Topic
=message.Topic,
WillRetain
=message.WillRetain
};
awaitPublish(deviceId, willMessage);awaitRemotePublish(deviceId, willMessage);
}
}

以下是测试结果:

三、总结

木舟IOT平台会在github开源社区版本,可以自由更改代码,用于商业项目,但不能自营平台,如低代码平台,IOT平台等,如有违反,后果自负,还有最好不要更改命名空间,然后跟公司说是自己研发的,如果知道后,我在博客全网通报此人,以前surging相关的事件就算了,就当没发生过。,如果碰到困难,比较紧急的话,可以联系作者,加群:744677125

本篇是 Python 系列教程第 3 篇,更多内容敬请访问我的 Python 合集

Visual Studio Code的安装非常简单,就不放这里增加文章篇幅了。

相比PyCharm,VSCode更加轻量,启动速度快。并且搭配Python插件就能实现和Pycharm一样的代码提示、高亮效果。

1 安装插件

安装插件也非常简单,打开VSCode->拓展->搜python->install

安装完成后重启一下VSCode

2 创建项目

VSCode中并不能像在PyCharm中直接创建项目就有虚拟环境。VSCode中我们要手动创建一个项目文件夹,然后在此文件夹内创建Python虚拟环境。

2.1 创建文件夹

用VSCode打开这个文件夹

这样我们就有了一个空的项目

创建一个hello.py文件,并打印Hello Python!!!

运行(右键运行或者点右上角运行按钮):

运行结果:

2.2 创建虚拟环境

首先讲一下什么是虚拟环境:虚拟环境的作用是让不同的Python项目使用不同的Python解释器、第三方库等,把项目和项目之间进行隔离,安装的软件包互不冲突。对于我们学习Python来说,用虚拟环境不是必须的。如果道友想再多一点了解虚拟环境可以查询专栏里《Python虚拟环境介绍》一文。

如何创建虚拟环境呢?

步骤:
Ctrl+P打开控制面板->输入
>python:
->选择Create Environment->选择Venv或Conda->选择Python解释器。接下来就开始创建了

2.3 配置debugger并debug

Debug是代码开发过程必备可少的操作,Python也有Debug模式。

step1:打断点。有两种方式,一是直接点击行号左侧,二是将鼠标放到这行然后按F9。

step2:选择调试器。按F5打开调试器列表,选择Python Debugger。

step3:选择调试器配置,第一项为“调试当前选中的文件”

step4:点击文件右上角的运行按钮旁边的向下箭头,选择Python Debugger: Debug Python File

能看到成功进入了Debug模式

使用FastAPI开发项目时,良好的目录结构可以帮助你更好地组织代码,提高可维护性和扩展性。同样,对基类的封装,也可以进一步减少开发代码,提供便利,并减少出错的几率。

下面是一个推荐的目录结构示例:

my_fastapi_project/├── app/│   ├── __init__.py
│ ├── main.py # 入口文件
│ ├── core
/│ │ ├── __init__.py
│ │ ├── config.py # 配置文件
│ │ ├── security.py # 安全相关
│ │ └── ... # 其他核心功能
│ ├── api
/│ │ ├── __init__.py
│ │ ├── v1
/│ │ │ ├── __init__.py
│ │ │ ├── endpoints
/│ │ │ │ ├── __init__.py
│ │ │ │ ├── users.py # 用户相关接口
│ │ │ │ ├── items.py # 其他接口
│ │ │ │ └── ...
│ │ │ └── ... # 其他版本的API
│ ├── models
/│ │ ├── __init__.py
│ │ ├── user.py # 用户模型
│ │ ├── item.py # 其他模型
│ │ └── ...
│ ├── schemas
/│ │ ├── __init__.py
│ │ ├── user.py # 用户数据模型
│ │ ├── item.py # 其他数据模型
│ │ └── ...
│ ├── crud
/│ │ ├── __init__.py
│ │ ├── user.py # 用户CRUD操作
│ │ ├── item.py # 其他CRUD操作
│ │ └── ...
│ ├── db
/│ │ ├── __init__.py
│ │ ├── base.py # 数据库基础设置
│ │ ├── session.py # 数据库会话
│ │ └── ...
│ ├── tests
/│ │ ├── __init__.py
│ │ ├── test_main.py # 测试主文件
│ │ ├── test_users.py # 用户相关测试
│ │ └── ...
│ └── utils
/│ ├── __init__.py
│ ├── utils.py # 工具函数
│ └── ...
├── .
env# 环境变量文件
├── alembic
/# 数据库迁移工具目录
│ ├──
env.py
│ ├── script.py.mako
│ └── versions
/│ └── ...
├── alembic.ini # Alembic 配置文件
├── requirements.txt # 项目依赖
├── Dockerfile # Docker 配置文件
└── README.md # 项目说明文件

目录结构说明:

  • app/
    : 项目的主目录,包含所有应用相关代码。
    • main.py
      : 项目的入口文件,启动FastAPI应用。
    • core/
      : 核心功能,如配置、安全等。
    • api/
      : API路由和视图,分版本管理。
    • models/
      : 数据库模型。
    • schemas/
      : 数据模型,用于请求和响应的验证。
    • crud/
      : 数据库操作(CRUD:创建、读取、更新、删除)。
    • db/
      : 数据库相关设置和会话管理。
    • tests/
      : 测试代码。
    • utils/
      : 工具函数和公用模块。
  • .env
    : 环境变量文件,用于存储敏感信息,如数据库连接字符串。
  • alembic/
    : 数据库迁移工具Alembic的配置目录。
  • requirements.txt
    : 项目依赖列表。
  • Dockerfile
    : Docker配置文件,用于容器化部署。
  • README.md
    : 项目说明文件。

这个结构可以根据项目需求进行调整,但保持清晰和模块化是良好的实践。

python项目总的__init__.py,有意义吗

在Python项目中,
__init__.py
文件的主要作用是将目录标识为一个Python包。它使得目录中的模块可以被导入和使用。在一些情况下,
__init__.py
可以不仅仅是一个空文件,还可以包含一些初始化代码。

__init__.py
的意义:

  1. 将目录标识为包:


    • 任何包含
      __init__.py
      的目录都会被Python解释器认为是一个包,这样你就可以使用包导入语法,如
      import mypackage.module
  2. 初始化代码:

  • 可以在
    __init__.py
    中包含一些初始化代码,如导入包内的子模块、设置包级别的变量或函数、配置日志记录等。例如:
# mypackage/__init__.py
from .submodule1 import func1
from .submodule2 import func2

__all__
= ["func1", "func2"]

3.简化导入

    • 通过在
      __init__.py
      中导入子模块,可以简化包的导入路径,使得用户可以直接从包中导入函数或类,而不必知道具体的模块结构。
# mypackage/__init__.py
from .submodule import MyClass

# Now you can
dofrom mypackage import MyClass

对于Python 3.3及以上版本,
__init__.py
文件不是强制性的,即使没有
__init__.py
文件,Python解释器也可以识别包。然而,添加
__init__.py
文件仍然是一个良好的习惯,可以避免某些情况下的意外行为,并且明确表示该目录是一个包。

2、Fast API项目的开发处理过程

在FastAPI项目中,CRUD操作通常在一个专门的
crud
模块中实现。这个模块会调用SQLAlchemy模型对象来进行数据库操作。

1. 定义模型 (
models/user.py
)

from sqlalchemy importColumn, Integer, Stringfrom app.db.base_class importBaseclassUser(Base):__tablename__ = "users"id= Column(Integer, primary_key=True, index=True)
email
= Column(String, unique=True, index=True, nullable=False)
hashed_password
= Column(String, nullable=False)
full_name
= Column(String, index=True)

2. 创建数据库会话 (
db/session.py
)

from sqlalchemy importcreate_enginefrom sqlalchemy.orm importsessionmaker

DATABASE_URL
= "sqlite:///./test.db" #使用SQLite数据库作为示例 engine=create_engine(DATABASE_URL)
SessionLocal
= sessionmaker(autocommit=False, autoflush=False, bind=engine)

3. 定义CRUD操作 (
crud/user.py
)

from sqlalchemy.orm importSessionfrom app.models.user importUserfrom app.schemas.user importUserCreate, UserUpdatedefget_user(db: Session, user_id: int):return db.query(User).filter(User.id ==user_id).first()defget_user_by_email(db: Session, email: str):return db.query(User).filter(User.email ==email).first()def get_users(db: Session, skip: int = 0, limit: int = 10):returndb.query(User).offset(skip).limit(limit).all()defcreate_user(db: Session, user: UserCreate):
db_user
=User(
email
=user.email,
hashed_password
=user.hashed_password, #在实际应用中应该对密码进行哈希处理 full_name=user.full_name
)
db.add(db_user)
db.commit()
db.refresh(db_user)
returndb_userdefupdate_user(db: Session, user_id: int, user: UserUpdate):
db_user
=get_user(db, user_id)ifdb_user:
db_user.email
=user.email
db_user.full_name
=user.full_name
db.commit()
db.refresh(db_user)
returndb_userdefdelete_user(db: Session, user_id: int):
db_user
=get_user(db, user_id)ifdb_user:
db.delete(db_user)
db.commit()
return db_user

4. 定义数据模型 (
schemas/user.py
)

from pydantic importBaseModelclassUserBase(BaseModel):
email: str
full_name: str
=NoneclassUserCreate(UserBase):
hashed_password: str
classUserUpdate(UserBase):pass classUser(UserBase):
id: int
classConfig:
orm_mode
= True

5. 在API端点中使用CRUD操作 (
api/v1/endpoints/users.py
)

from fastapi importAPIRouter, Depends, HTTPExceptionfrom sqlalchemy.orm importSessionfrom app importcrud, models, schemasfrom app.db.session importSessionLocal

router
=APIRouter()defget_db():
db
=SessionLocal()try:yielddbfinally:
db.close()

@router.post(
"/users/", response_model=schemas.User)def create_user(user: schemas.UserCreate, db: Session =Depends(get_db)):
db_user
= crud.get_user_by_email(db, email=user.email)ifdb_user:raise HTTPException(status_code=400, detail="Email already registered")return crud.create_user(db=db, user=user)

@router.get(
"/users/{user_id}", response_model=schemas.User)def read_user(user_id: int, db: Session =Depends(get_db)):
db_user
= crud.get_user(db, user_id=user_id)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")returndb_user

@router.put(
"/users/{user_id}", response_model=schemas.User)def update_user(user_id: int, user: schemas.UserUpdate, db: Session =Depends(get_db)):
db_user
= crud.update_user(db=db, user_id=user_id, user=user)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")returndb_user

@router.delete(
"/users/{user_id}", response_model=schemas.User)def delete_user(user_id: int, db: Session =Depends(get_db)):
db_user
= crud.delete_user(db=db, user_id=user_id)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")returndb_user

@router.get(
"/users/", response_model=List[schemas.User])def read_users(skip: int = 0, limit: int = 10, db: Session =Depends(get_db)):
users
= crud.get_users(db, skip=skip, limit=limit)return users

6. 注册路由 (
main.py
)

from fastapi importFastAPIfrom app.api.v1.endpoints importusers

app
=FastAPI()

app.include_router(users.router, prefix
="/api/v1", tags=["users"])if __name__ == "__main__":importuvicorn
uvicorn.run(app, host
="0.0.0.0", port=8000)

7. 初始化数据库 (
db/base.py
)

from app.db.session importenginefrom app.models importuser

user.Base.metadata.create_all(bind
=engine)

8. 运行应用

在项目根目录下运行:

uvicorn app.main:app --reload

这样,你的CRUD层就可以调用模型对象来进行数据库操作了。上述代码展示了如何定义模型、数据库会话、CRUD操作、数据模型和API端点,并将它们结合在一起,实现一个简单的用户管理系统。

3、实际FastAPI项目对基类的封装

可以通过创建一个通用的CRUD基类来封装常规的CRUD操作,然后让特定的CRUD类继承这个基类。这样可以减少重复代码,提高代码的可维护性和可复用性。下面是一个实现示例。

1、创建通用CRUD基类 (
crud/base.py
)

rom typing importGeneric, Type, TypeVar, Optional, Listfrom pydantic importBaseModelfrom sqlalchemy.orm importSessionfrom app.db.base_class importBase

ModelType
= TypeVar("ModelType", bound=Base)
CreateSchemaType
= TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType
= TypeVar("UpdateSchemaType", bound=BaseModel)classCRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):def __init__(self, model: Type[ModelType]):
self.model
=modeldef get(self, db: Session, id: int) ->Optional[ModelType]:return db.query(self.model).filter(self.model.id ==id).first()def get_multi(self, db: Session, skip: int = 0, limit: int = 100) ->List[ModelType]:returndb.query(self.model).offset(skip).limit(limit).all()def create(self, db: Session, obj_in: CreateSchemaType) ->ModelType:
obj_in_data
=obj_in.dict()
db_obj
= self.model(**obj_in_data) #type: ignore db.add(db_obj)
db.commit()
db.refresh(db_obj)
returndb_objdef update(self, db: Session, db_obj: ModelType, obj_in: UpdateSchemaType) ->ModelType:
obj_data
=db_obj.dict()
update_data
= obj_in.dict(skip_defaults=True)for field inobj_data:if field inupdate_data:
setattr(db_obj, field, update_data[field])
db.commit()
db.refresh(db_obj)
returndb_objdef remove(self, db: Session, id: int) ->ModelType:
obj
=db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj

2、定义用户CRUD操作 (
crud/user.py
)

from typing importAnyfrom sqlalchemy.orm importSessionfrom app.crud.base importCRUDBasefrom app.models.user importUserfrom app.schemas.user importUserCreate, UserUpdateclassCRUDUser(CRUDBase[User, UserCreate, UserUpdate]):def get_by_email(self, db: Session, email: str) ->Any:return db.query(self.model).filter(self.model.email ==email).first()

user
= CRUDUser(User)

3、定义数据模型 (
schemas/user.py
)

from pydantic importBaseModelclassUserBase(BaseModel):
email: str
full_name: str
=NoneclassUserCreate(UserBase):
hashed_password: str
classUserUpdate(UserBase):pass classUser(UserBase):
id: int
classConfig:
orm_mode
= True

4、在API端点中使用CRUD操作 (
api/v1/endpoints/users.py
)

from fastapi importAPIRouter, Depends, HTTPExceptionfrom sqlalchemy.orm importSessionfrom typing importListfrom app importcrud, schemasfrom app.db.session importSessionLocalfrom app.models.user importUser

router
=APIRouter()defget_db():
db
=SessionLocal()try:yielddbfinally:
db.close()

@router.post(
"/users/", response_model=schemas.User)def create_user(user: schemas.UserCreate, db: Session =Depends(get_db)):
db_user
= crud.user.get_by_email(db, email=user.email)ifdb_user:raise HTTPException(status_code=400, detail="Email already registered")return crud.user.create(db=db, obj_in=user)

@router.get(
"/users/{user_id}", response_model=schemas.User)def read_user(user_id: int, db: Session =Depends(get_db)):
db_user
= crud.user.get(db, id=user_id)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")returndb_user

@router.put(
"/users/{user_id}", response_model=schemas.User)def update_user(user_id: int, user: schemas.UserUpdate, db: Session =Depends(get_db)):
db_user
= crud.user.get(db=db, id=user_id)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")return crud.user.update(db=db, db_obj=db_user, obj_in=user)

@router.delete(
"/users/{user_id}", response_model=schemas.User)def delete_user(user_id: int, db: Session =Depends(get_db)):
db_user
= crud.user.get(db=db, id=user_id)if db_user isNone:raise HTTPException(status_code=404, detail="User not found")return crud.user.remove(db=db, id=user_id)

@router.get(
"/users/", response_model=List[schemas.User])def read_users(skip: int = 0, limit: int = 10, db: Session =Depends(get_db)):
users
= crud.user.get_multi(db, skip=skip, limit=limit)return users

其他的就是类似前面的做法了。

通过这种方式,你可以在通用的CRUD基类中封装常规的CRUD操作,而特定的CRUD类(如
CRUDUser
)只需要继承这个基类并添加特定的操作方法。这样不仅减少了重复代码,也提高了代码的可维护性和可复用性。

如果你希望可以通过定义一个通用的API基类来封装常规的CRUD操作方法,然后在具体的端点文件中继承这个基类。这样可以进一步减少重复代码,提高代码的可维护性和可复用性。

创建通用API基类 (
api/deps.py
)

from typing importType, TypeVar, Generic, Listfrom fastapi importAPIRouter, Depends, HTTPExceptionfrom sqlalchemy.orm importSessionfrom pydantic importBaseModelfrom app.crud.base importCRUDBasefrom app.db.session importSessionLocal

ModelType
= TypeVar("ModelType", bound=BaseModel)
CreateSchemaType
= TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType
= TypeVar("UpdateSchemaType", bound=BaseModel)classCRUDRouter(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):def __init__(self, crud: CRUDBase[ModelType, CreateSchemaType, UpdateSchemaType]):
self.crud
=crud
self.router
=APIRouter()

self.router.post(
"/", response_model=ModelType)(self.create_item)
self.router.get(
"/{item_id}", response_model=ModelType)(self.read_item)
self.router.put(
"/{item_id}", response_model=ModelType)(self.update_item)
self.router.delete(
"/{item_id}", response_model=ModelType)(self.delete_item)
self.router.get(
"/", response_model=List[ModelType])(self.read_items)defget_db(self):
db
=SessionLocal()try:yielddbfinally:
db.close()

async
def create_item(self, item_in: CreateSchemaType, db: Session =Depends(self.get_db)):
db_item
= self.crud.create(db=db, obj_in=item_in)returndb_item

async
def read_item(self, item_id: int, db: Session =Depends(self.get_db)):
db_item
= self.crud.get(db=db, id=item_id)if notdb_item:raise HTTPException(status_code=404, detail="Item not found")returndb_item

async
def update_item(self, item_id: int, item_in: UpdateSchemaType, db: Session =Depends(self.get_db)):
db_item
= self.crud.get(db=db, id=item_id)if notdb_item:raise HTTPException(status_code=404, detail="Item not found")return self.crud.update(db=db, db_obj=db_item, obj_in=item_in)

async
def delete_item(self, item_id: int, db: Session =Depends(self.get_db)):
db_item
= self.crud.get(db=db, id=item_id)if notdb_item:raise HTTPException(status_code=404, detail="Item not found")return self.crud.remove(db=db, id=item_id)

async
def read_items(self, skip: int = 0, limit: int = 10, db: Session =Depends(self.get_db)):
items
= self.crud.get_multi(db=db, skip=skip, limit=limit)return items

使用通用API基类定义用户端点(
api/v1/endpoints/users.py
)

from fastapi importAPIRouterfrom app.crud.user importuser as user_crudfrom app.schemas.user importUser, UserCreate, UserUpdatefrom app.api.deps importCRUDRouter

user_router
=CRUDRouter[User, UserCreate, UserUpdate](user_crud)
router
= user_router.router

注册路由 (
main.py
)

rom fastapi importFastAPIfrom app.api.v1.endpoints importusers

app
=FastAPI()

app.include_router(users.router, prefix
="/api/v1/users", tags=["users"])if __name__ == "__main__":importuvicorn
uvicorn.run(app, host
="0.0.0.0", port=8000)

通过这种方式,你可以在
CRUDRouter
基类中封装常规的CRUD操作方法,然后在具体的端点文件中继承这个基类并传递相应的CRUD对象。这样可以进一步减少重复代码,提高代码的可维护性和可复用性。

4、SQLAlchemy模型的基类定义

app.db.base_class
通常是用于定义SQLAlchemy模型基类的文件。在这个文件中,我们会定义一个基本的Base类,这个类是所有SQLAlchemy模型的基类。下面是一个实现示例:

定义
Base
类 (
db/base_class.py
)

from sqlalchemy.ext.declarative importas_declarative, declared_attr

@as_declarative()
classBase:
id: int
__name__: str

@declared_attr
def __tablename__(cls) ->str:return cls.__name__.lower()

详细解释

  1. @as_declarative()
    : 这是SQLAlchemy提供的一个装饰器,它会将类装饰为一个声明性基类。所有继承自这个类的子类都会自动成为声明性类。

  2. id: int
    : 这是一个类型注释,表示每个模型类都会有一个
    id
    属性。具体的字段定义(例如
    Column(Integer, primary_key=True)
    )会在每个具体的模型类中定义。

  3. __name__: str
    : 这是另一个类型注释,表示每个模型类都会有一个
    __name__
    属性。

  4. @declared_attr
    : 这是SQLAlchemy提供的一个装饰器,允许我们为声明性基类定义一些通用的属性。在这个例子中,它用于自动生成
    __tablename__
    属性。这个属性的值是模型类的名称的小写形式。

这样定义的
Base
类可以作为所有SQLAlchemy模型的基类,简化模型的定义。

完整示例项目结构:为了更好地理解,这里展示一个包含
Base
类定义的完整项目结构:

.
├── app
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ └── v1
│ │ ├── __init__.py
│ │ └── endpoints
│ │ ├── __init__.py
│ │ └── users.py
│ ├── crud
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── user.py
│ ├── db
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── base_class.py
│ │ └── session.py
│ ├── models
│ │ ├── __init__.py
│ │ └── user.py
│ ├── schemas
│ │ ├── __init__.py
│ │ └── user.py
│ └── main.py

models/user.py 类文件如下定义

from sqlalchemy importColumn, Integer, Stringfrom app.db.base_class importBaseclassUser(Base):__tablename__ = "users"id= Column(Integer, primary_key=True, index=True)
email
= Column(String, unique=True, index=True, nullable=False)
hashed_password
= Column(String, nullable=False)
full_name
= Column(String, index=True)

通过这种结构和定义,您可以创建一个简洁、可扩展的FastAPI项目,能够快速定义新的数据库模型并生成相应的CRUD操作和API端点。