工业仿真(simulation)--仿真引擎,离散事件仿真(1)

接下来我来介绍整个仿真软件最重要,最核心的部分,驱动引擎

🤔设计思路

第一步:自主研发的萌芽 在刚开始开发仿真引擎时,我原本是想找一个现有的JS离散事件仿真库,但最后发现要么有的库已经长时间不再更新,无法满足现有需求,要么就是功能不完善,无法实现工厂流水线式的仿真,经过思考后,选择自主研发一个离散时间仿真库

第二步:初稿确立 关于如何确定项目结构,以及仿真引擎采用什么样的数据结构,这个我问询了GPT,他给了我一个初版

先确定产品整个的加工流程,然后将流程导入到产品类中,开始仿真的时候,产品就可以根据这个流程一步一步加工

这样第一个问题就来了,我们是离散事件,不可能确定产品下一步是在哪个设备上进行加工的,只能动态的去判断,所以面临这个问题时,第一个初版就要被推翻,重新设计

第三步:流程的优化 如果要想知道产品下一步流入到哪个设备中,就需要建立一条完整的仿真链路,让设备与设备之间相互关联,类似于数据结构中的链表

首先是我给每台设备附加了一个状态属性(status),用来指示当前设备处于什么状态

  • 'processing':正在加工状态
  • 'idle' :空闲状态
  • 'ready' :准备接收产品状态
  • 'block' :堵塞状态,当产品加工完毕时,就会处于堵塞状态
  • 'clearance' :需要进行清理状态,比如产品发生了不良
  • 'fault':故障状态

我可以通过这些状态来判断当前设备是否可以接收产品,是否可以派发产品

接下来我给设备附加了一个下一站的属性数据(nextStations),这是一个列表结构,因为设备下一站不仅仅有一台设备,当产品加工完毕时,就需要循环遍历下一站的列表,判断哪个设备处于空闲状态,就将产品派发给他

这样就会产生第二个问题

设备A【堵塞状态】 --> 设备B【正在加工】

当设备A加工完毕,正准备把产品送给设备B时,设备B处于正在加工,不能接收产品,那么设备A就会一直处于堵塞,当设备B加工完毕后,就会转为空闲状态,但是并不会主动向设备A请求产品,这就是问题所在,除非把设备之间的单向通信改为双向通信

第四步:最终版 经过思考后,决定在设备上再次附加一个属性,类似于nextStations,这次是上一站的属性数据(prevStations ),也是一个列表结构,到此,我们的整个仿真链路已经形成,本质就是双向链表

就拿上面的案例来说,当设备B加工完毕时,设备B转为空闲,此时就会向设备A索要产品,如果设备A处于堵塞状态,就会将产品派发给设备B,如果处于其他状态,将拒绝产品派发。

那接下来我们看一下项目结构

📋项目结构

如图一图二所示,这是整个仿真引擎的整体架构

图一

图二

下面我详细说明一下每个文件夹,每个文件的含义

  • core //仿真核心部件
  1. Dispatcher //设备调度器
  2. EventQueue //事件队列
  3. ReadyProducts //就绪产品队列
  4. Scheduler //事件单元
  5. SimulationClock // 当前仿真的时间
  6. SimulationSpeed //当前仿真的速度
  • entities //仿真实体类【在这篇文章中主要讲解发生器,吸收器,加Buffer工站】
  1. AGV //搬运流实体类
  2. AssemblyStation //装配站实体类
  3. BaseStation //基类
  4. BatchProcess //批处理设备实体类
  5. Buffer //缓冲区实体类
  6. Conveyor //传送带实体类
  7. DisassemblyStation //拆卸站实体类
  8. Generator //发生器实体类
  9. Product //产品实体类
  10. Sink //吸收器实体类
  11. Worker //工人实体类
  12. WorkerPool //工人池实体类
  13. Workstation //加工站实体类
  • messaging
  1. MessageTransfer //仿真数据传递给画布的代理方法
  • simulation
  1. ModelBuilder //仿真建模
  2. SimController //仿真控制器
  • types //各种类型断言
  • utils //工具包

💫设计模式

由于我们的仿真引擎是一个比较复杂的系统,所以在设计阶段就必须采用一个合适的设计模式,我们可以联想到,无论是加工站,发生器,吸收器,还是缓冲区,传送带,他们都有一个本质上的相同点,都是接收产品,然后加工一段时间,再将产品派发出去

那么我们就可以提取这些共同点,将他们剥离出来,形成一个父类,这些实体类继承这个父类,这就是工厂模式

下面是工厂模式的简介

1. 简单工厂模式(Simple Factory)

  • 不是标准的GOF 23种设计模式,但非常常用,是工厂方法模式的简化。
  • 核心:定义一个工厂类,它根据传入的参数,动态决定创建哪一种产品类的实例。
  • 比喻:有一个"万能充电器"(工厂),你给它参数(比如苹果口、Type-C口),它就给你对应的充电线(产品)。
  • 优点:客户端与具体产品解耦,职责清晰。
  • 缺点:违反"开闭原则"(对扩展开放,对修改关闭)。如果要增加新产品,必须修改工厂类的逻辑。

2. 工厂方法模式(Factory Method)

  • 核心:定义一个用于创建对象的接口(工厂接口),但让子类决定实例化哪一个产品类。工厂方法使一个类的实例化延迟到其子类。
  • 比喻:有一个"充电器店"的抽象概念(工厂接口)。具体的"苹果专卖店"(具体工厂)只卖苹果充电线(具体产品), "小米专卖店"(具体工厂)只卖Type-C充电线(具体产品)。你想买什么线,就去对应的店。
  • 优点:完全遵循"开闭原则"。需要增加新产品时,只需新建一个产品类和对应的工厂子类,无需修改已有代码。
  • 缺点:类的数量会增多,系统变得更复杂。

3. 抽象工厂模式(Abstract Factory)

  • 核心:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
  • 比喻:这是一个"家电生态工厂"的概念。有"小米生态工厂"(具体工厂),它能生产小米手机、小米电视、小米空调(一系列相关产品)。也有"苹果生态工厂"(具体工厂),它能生产苹果手机、苹果电脑、苹果手表(另一系列相关产品)。客户端选择一个品牌(工厂),就能得到该品牌的整套产品。
  • 优点:能确保客户端始终只使用同一个产品族中的对象。
  • 缺点:难以支持新种类的产品。例如,如果要在抽象工厂里增加一个"智能汽车"产品,那么所有具体工厂(小米、苹果)都需要修改。

💻代码

接下来我们来看实体类所继承的父类,也就是

BaseStation.ts

ts 复制代码
import { currentTime } from '../core/SimulationClock'
import Product from './Product'
import { getReadyProduct } from '../core/ReadyProducts'
import { StationStatus } from '../types'
import { dispatcher } from '../core/Dispatcher'

abstract class BaseStation {
  id: string
  name: string
  x: number
  y: number
  width: number
  height: number
  prevStations: BaseStation[] = []
  nextStations: BaseStation[] = []
  status: StationStatus = 'idle'
  readyProduct: null | string = null

  constructor(id: string, name: string, x: number, y: number, width = 100, height = 100) {
    this.id = id
    this.name = name
    this.x = x
    this.y = y
    this.width = width
    this.height = height
  }

  //设置下游设备
  setNextStations(stations: BaseStation[]) {
    this.nextStations = stations
  }

  //设置上游设备
  setPrevStations(stations: BaseStation[]) {
    this.prevStations = stations
  }

  //接受就绪产品
  receiveReadyProduct(productId: string): void {
    if (this.status === 'idle') {
      this.readyProduct = productId
      this.setStatus('ready')

      const product = getReadyProduct(productId)
      if (!product) {
        this.setStatus('idle')
        console.log(`[${currentTime}] ❌ ${productId} 没有发现该产品`)
        return
      }
      console.log(`[${currentTime}] ${product.id} 已到达 --> ${this.name}`)
      product.setFrom(this.id)
      this.onProductReceived(product)
    } else {
      console.log(`[${currentTime}] ${this.id} 不在空闲状态, 无法接受产品 ${productId}`)
    }
  }

  // 子类必须实现
  public abstract canReceiveProduct(id: string, product: Product): boolean
  protected abstract onProductReceived(product: Product): void
  public abstract tryDispatchCurrentProduct(): void

  protected setStatus(newStatus: StationStatus) {
    if (this.status !== newStatus) {
      this.status = newStatus
      dispatcher.notifyStatusChange(this, newStatus)
    }
  }

  /**
   * 外部调用本部事件,需要通过一个事件总线来通知
   */
  public eventWindow(eventType: 'status' | 'rework' | 'delProduct', param: any): void {
    return
  }
}

export default BaseStation

这里面有几个重要的属性和方法需要说明一下

  • id: string //实体类唯一ID
  • name: string //实体类名称
  • x: number //实体类位置
  • y: number
  • width: number //实体类尺寸
  • height: number
  • prevStations: BaseStation[] = [] //上一站
  • nextStations: BaseStation[] = [] //下一站
  • status: StationStatus = 'idle' //当前状态

方法

  1. canReceiveProduct():当前设备是否可以接收产品,
  2. receiveReadyProduct():当可以接收产品后,上一站就会派发产品,通过这个方法来接收就绪的产品
  3. onProductReceived():真正获取到产品,receiveReadyProduct方法只是拿到就绪的产品,因为上一站派发产品时,会将产品移交到就绪产品队列中,receiveReadyProduct方法会从就绪产品队列里面拿到产品,然后移交到onProductReceived方法
  4. tryDispatchCurrentProduct():派发产品方法
  5. setStatus():状态改变

接下来我们拿加工站实体类来举例

Workstation.ts

ts 复制代码
import { addReadyProduct } from '../core/ReadyProducts'
import { schedule } from '../core/Scheduler'
import { currentTime } from '../core/SimulationClock'
import { messageTransfer } from '../messaging/MessageTransfer'
import BaseStation from './BaseStation'
import Product from './Product'

class Workstation extends BaseStation {
  //加工时间
  processTime: number | string
  //不良品率
  defectRate: number
  //当前正在加工的产品
  currentProduct: Product | null = null
  //故障率
  faultRate: number = 0

  constructor(
    id: string,
    name: string,
    x: number,
    y: number,
    width = 100,
    height = 100,
    processTime: number | string,
    defectRate: number = 0,
    faultRate: number = 0
  ) {
    super(id, name, x, y, width, height)
    this.processTime = processTime
    this.defectRate = defectRate
    this.faultRate = faultRate
  }

  public canReceiveProduct(): boolean {
    return this.status === 'idle'
  }

  //接收到产品,进行加工前的准备工作
  onProductReceived(product: Product): void {
    messageTransfer('product', 'move', { targetId: this.id, productId: product.id })
    messageTransfer('product', 'startProcessing', { targetId: this.id, productId: product.id })
    this.tryProcess(product)
  }

  tryProcess(product: Product, retry = false): void {
    this.currentProduct = product
    this.setStatus('processing')
    if (Math.random() < this.faultRate && !retry) {
      messageTransfer('style', null, {
        targetId: this.id,
        style: {
          backgroundColor: '#FFB1B16B'
        }
      })
      messageTransfer('product', 'finishProcessing', { targetId: this.id, productId: product.id })
      this.setStatus('fault')
      return
    }

    let time = 0
    if (typeof this.processTime === 'string') {
      time = new Function(this.processTime)()
    } else {
      time = this.processTime
    }
    console.log(`[${currentTime}] ${this.name} 开始加工 ${product.id},预计需要时间 ${time}`)
    schedule(time, () => this.finishProcessing(product), `${this.id} process finish ${product.id}`)
  }

  finishProcessing(product: Product): void {
    if (Math.random() < this.defectRate) {
      console.log(`[${currentTime}] ❌ ${this.name} 报废产品 ${product.id}`)
      messageTransfer('style', null, {
        targetId: this.id,
        style: {
          backgroundColor: '#FFDEB16B'
        }
      })
      messageTransfer('product', 'finishProcessing', { targetId: this.id, productId: product.id })
      this.setStatus('clearance')
      return
    }
    messageTransfer('product', 'finishProcessing', { targetId: this.id, productId: product.id })
    //产品加工完毕,尝试派发产品
    this.setStatus('block')
  }

  //向下游派发产品
  public tryDispatchCurrentProduct(): void {
    if (!this.currentProduct) return
    console.log(`[${currentTime}] ${this.name} 尝试派发产品 ${this.currentProduct.id}`)
    const productId = this.currentProduct.id
    for (const next of this.nextStations) {
      if (next.canReceiveProduct(this.id, this.currentProduct)) {
        //当前产品添加到就绪产品队列中
        addReadyProduct(this.currentProduct)
        this.currentProduct = null

        next.receiveReadyProduct(productId)
        this.setStatus('idle')
        break
      }
    }
  }

  public eventWindow(eventType: 'status' | 'rework' | 'delProduct', param: any): void {
    if (eventType === 'status') {
      this.setStatus(param)
    } else if (eventType === 'rework' && this.currentProduct) {
      messageTransfer('product', 'startProcessing', {
        targetId: this.id,
        productId: this.currentProduct.id
      })
      this.tryProcess(this.currentProduct, true)
    } else if (eventType === 'delProduct' && this.currentProduct) {
      messageTransfer('product', 'recycle', {
        targetId: this.id,
        productId: this.currentProduct.id
      })
    }
  }
}

export default Workstation

可以看到,加工站继承了BaseStation,同时重写抽象方法

并且有自己独立的数据处理方法

  • tryProcess
  • finishProcessing
  1. 从加工站实体类我们就可以看出,当canReceiveProduct方法被调用时,会根据自身是否参与空闲状态来返回Boolean值

  2. 然后接收到产品后,会调用tryProcess方法进行加工

  3. tryProcess()会将加工事件移交到事件队列里面,Scheduler后面我们会讲到

  4. 加工完毕后,我们会调用finishProcessing方法,完成加工,然后在调用产品派发方法

  5. 在产品派发方法里面,我们会循环遍历下一站,通过canReceiveProduct ()判断下一站哪一个处于空闲状态,如果有,就将产品通过receiveReadyProduct()派发给他,然后改变设备状态

这时我们就发现了,我们在设备加工完毕后,只是将产品派发给了下一站,并没有向上一站索要产品,那么就引出了我们的设备调度器Dispatcher.ts

在我们的基类里面,我们可以看到我们的状态改变后,会调用调度器的notifyStatusChange方法

ts 复制代码
  protected setStatus(newStatus: StationStatus) {
    if (this.status !== newStatus) {
      this.status = newStatus
      dispatcher.notifyStatusChange(this, newStatus)
    }
  }

Dispatcher.ts

ts 复制代码
import BaseStation from '../entities/BaseStation'
import WorkerPool from '../entities/WorkerPool'
import workerPool from '../entities/WorkerPool'
import { StationStatus } from '../types'

class Dispatcher {
  //空闲设备
  public idleStations = new Set<BaseStation>()
  //阻塞设备
  private blockedStations = new Set<BaseStation>()
  //特殊设备
  private specialStations = new Set<BaseStation>()
  //清理状态设备
  private clearStations = new Set<BaseStation>()
  //故障设备
  private faultStations = new Set<BaseStation>()
  //工人池
  public workerPoolList = new Set<workerPool>()

  registerStation(station: BaseStation) {
    // 可选:记录拓扑、分组、类别等
  }

  // 注册空闲基站
  registerIdleStation(stations: BaseStation[]) {
    // 遍历基站数组
    stations.forEach((station) => {
      // 将基站添加到空闲基站集合中
      this.idleStations.add(station)
    })
  }

  // 注册特殊基站
  registerSpecialStation(station: BaseStation) {
    this.specialStations.add(station)
  }

  // 注册工人池
  registerWorkerPool(workerPool: workerPool) {
    this.workerPoolList.add(workerPool)
  }

  /**
   * 有设备状态发生改变时,通知调度器
   * @param station
   * @param status
   */
  notifyStatusChange(station: BaseStation, status: StationStatus) {
    this.removeFromAllSets(station)

    if (status === 'idle') this.idleStations.add(station)
    if (status === 'block') this.blockedStations.add(station)
    if (status === 'clearance') this.clearStations.add(station)
    if (status === 'fault') this.faultStations.add(station)

    this.tryResolve()
  }

  /**
   * 尝试解决堵塞和空闲设备
   */
  tryResolve() {
    /**
     * 解决堵塞设备,将产品流入到下一站
     */
    for (const producer of this.blockedStations) {
      if (producer.nextStations) {
        for (const consumer of producer.nextStations) {
          if (this.idleStations.has(consumer) || this.specialStations.has(consumer)) {
            producer.tryDispatchCurrentProduct()
            break
          }
        }
      }
    }

    /**
     * 解决空闲设备,向上游索要产品
     */
    for (const consumer of this.idleStations) {
      if (consumer.prevStations) {
        for (const supplier of consumer.prevStations) {
          if (this.blockedStations.has(supplier) || this.specialStations.has(supplier)) {
            supplier.tryDispatchCurrentProduct()
            break
          }
        }
      }
    }

    /**
     * 解决需要清理的设备,向工人池发送请求
     */
    for (const alarm of this.clearStations) {
      let workerPool = null as null | WorkerPool
      for (const pool of this.workerPoolList) {
        if (pool.isDeviceInMap(alarm.id)) {
          workerPool = pool
          break
        }
      }
      if (workerPool) {
        workerPool.receiveTask('clean', alarm)
      }
    }

    /**
     * 解决故障设备,向工人池发送请求
     */
    for (const fault of this.faultStations) {
      let workerPool = null as null | WorkerPool
      for (const pool of this.workerPoolList) {
        if (pool.isDeviceInMap(fault.id)) {
          workerPool = pool
          break
        }
      }
      if (workerPool) {
        workerPool.receiveTask('maintenance', fault)
      }
    }
  }

  //把某台设备从状态集合中删除
  private removeFromAllSets(station: BaseStation) {
    this.idleStations.delete(station)
    this.blockedStations.delete(station)
    this.clearStations.delete(station)
    this.faultStations.delete(station)
  }

  //清除某台设备的负面状态
  clearNegativeStatus(station: BaseStation) {
    this.clearStations.delete(station)
    this.faultStations.delete(station)
  }

  //清空所有数据
  clear() {
    this.idleStations.clear()
    this.blockedStations.clear()
    this.clearStations.clear()
    this.faultStations.clear()
    this.specialStations.clear()
  }

  //判断某个设备是否在堵塞站里面或者在特殊站里面
  isDeviceInBlockOrSpecial(station: BaseStation) {
    return this.blockedStations.has(station) || this.specialStations.has(station)
  }
}

export const dispatcher = new Dispatcher()

调度器的 tryResolve() 很重要,它会遍历所有的堵塞状态的设备,然后再遍历他们的下一站,并将产品传递给下一站,同时也会遍历所有的空闲状态的设备,然后再遍历他们的上一站,如果上一站处于堵塞状态,那么就会触发上一站产品派发

那接下来我们来看我们的事件队列,是怎么安排每个事件的执行顺序的

EventQueue.ts

ts 复制代码
import { MinPriorityQueue } from '@datastructures-js/priority-queue'

type Action = () => void

export class Event {
  constructor(
    public time: number,
    public action: Action,
    public description: string = ''
  ) {}
}

export const eventQueue = new MinPriorityQueue<Event>({
  compare: (a, b) => a.time - b.time
})
  • time:执行时间
  • action:执行事件
  • description:描述

Scheduler.ts

ts 复制代码
import { Event, eventQueue } from './EventQueue'
import { currentTime } from './SimulationClock'

type Action = () => void

export const schedule = (timeOffset: number, action: Action, description: string): void => {
  eventQueue.enqueue(new Event(currentTime + timeOffset, action, description))
}

在Workstation里面,我们会去调用schedule方法,去添加事件,在这个方法内部,会将这个事件添加到eventQueue队列里面,我们在new 出 eventQueue时,已经将这个事件进行来排序,根据时间来排序

那么接下,我们已经有了仿真的队列,怎么去执行这个队列里面的事件呢,这就要说到我们的仿真控制器

SimController.ts

ts 复制代码
// SimulationController.ts
import { eventQueue } from '../core/EventQueue'
import { setCurrentTime } from '../core/SimulationClock'
import { delay } from '../utils/Delay'
import SimulationSpeed from '../core/SimulationSpeed'
import { modeling } from './ModelBuilder'
import { dispatcher } from '../core/Dispatcher'
import { clearReadyProducts } from '../core/ReadyProducts'
import { messageTransfer } from '../messaging/MessageTransfer'

type SimStatus = 'idle' | 'running' | 'paused' | 'stopped'

class SimulationController {
  private status: SimStatus = 'idle'
  private pausedResolver: (() => void) | null = null
  private shouldStop: boolean = false

  public setStatus(status: 'running' | 'paused' | 'stopped' | 'resume'): void {
    if (status === 'running') {
      this.start()
    } else if (status === 'paused') {
      this.pause()
    } else if (status === 'stopped') {
      this.stop()
    } else if (status === 'resume') {
      this.resume()
    }
  }
  public changeSpeed(speed: number): void {
    SimulationSpeed.setSpeed = Number(speed)
  }

  getStatus(): SimStatus {
    return this.status
  }

  async start(): Promise<void> {
    //清空一下数据
    this.clearAll()
    if (this.status !== 'idle' && this.status !== 'stopped') return

    console.log('[Sim] ✅ 开始仿真')
    this.status = 'running'
    this.shouldStop = false
    let lastEventTime = 0
    modeling() // 初始化模型(只运行一次)
    while (!eventQueue.isEmpty()) {
      if (this.shouldStop) break
      if (this.status === ('paused' as SimStatus)) {
        await this.waitUntilResume()
      }

      const event = eventQueue.dequeue()
      if (event) {
        const simDelay = event.time - lastEventTime
        lastEventTime = event.time
        const waitMs = (simDelay * 1000) / SimulationSpeed.getSpeed
        await delay(waitMs)
        //发送仿真时间
        messageTransfer('simTime', null, event.time)
        setCurrentTime(event.time)
        event.action()
      }
    }

    if (this.status !== ('paused' as SimStatus)) {
      this.status = 'stopped'
      console.log('[Sim] 🛑 仿真完成或已停止')
    }
  }

  pause(): void {
    if (this.status === 'running') {
      this.status = 'paused'
      console.log('[Sim] ⏸️ 暂停')
    }
  }

  resume(): void {
    if (this.status === 'paused') {
      this.status = 'running'
      console.log('[Sim] ▶️ 重启')
      this.pausedResolver?.()
      this.pausedResolver = null
    }
  }

  stop(): void {
    this.shouldStop = true
    this.status = 'stopped'
    this.clearAll()
    console.log('[Sim] ❌ 仿真停止')
  }

  private waitUntilResume(): Promise<void> {
    return new Promise((resolve) => {
      this.pausedResolver = resolve
    })
  }

  private clearAll(): void {
    //清空队列
    eventQueue.clear()
    //重置时间
    setCurrentTime(0)
    //重置速度
    SimulationSpeed.setSpeed = 1
    //重置模型状态
    dispatcher.clear()
    //清空就绪产品队列
    clearReadyProducts()
  }
}

export const simController = new SimulationController()

我们主要看仿真控制器的这一段代码

ts 复制代码
while (!eventQueue.isEmpty()) {
  if (this.shouldStop) break
  if (this.status === ('paused' as SimStatus)) {
    await this.waitUntilResume()
  }

  const event = eventQueue.dequeue()
  if (event) {
    const simDelay = event.time - lastEventTime
    lastEventTime = event.time
    const waitMs = (simDelay * 1000) / SimulationSpeed.getSpeed
    await delay(waitMs)
    //发送仿真时间
    messageTransfer('simTime', null, event.time)
    setCurrentTime(event.time)
    event.action()
  }
}

首先他会循环遍历eventQueue队列,直到他为空

然后提取到里面的时间,并结合仿真速度,进行delay

最后执行对应的事件

一个基本的仿真流程就执行完毕了,由于篇幅过长,后续内容我会放到后面来讲,谢谢大家观看

相关推荐
Java微观世界3 小时前
匿名内部类和 Lambda 表达式为何要求外部变量是 final 或等效 final?原理与解决方案
java·后端
SimonKing3 小时前
全面解决中文乱码问题:从诊断到根治
java·后端·程序员
几颗流星3 小时前
Java 中使用 CountDownLatch 增加线程竞争,帮助复现并发问题
后端
郑洁文3 小时前
基于SpringBoot的天气预报系统的设计与实现
java·spring boot·后端·毕设
optimistic_chen4 小时前
【Java EE进阶 --- SpringBoot】Spring DI详解
spring boot·笔记·后端·spring·java-ee·mvc·di
Java水解4 小时前
【MySQL】数据库基础
后端·mysql
中国胖子风清扬4 小时前
Rust 日志库完全指南:从入门到精通
spring boot·后端·rust·学习方法·logback
玉衡子4 小时前
MySQL基础架构全面解析
数据库·后端
郭京京5 小时前
goweb内置的 net/http 包
后端·go