E:\song\agv_fastapi_socket_v4_mock\app.py
| import time |
| from fastapi import FastAPI, WebSocket, Request, WebSocketDisconnect |
| from fastapi.responses import RedirectResponse, HTMLResponse |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.openapi.docs import get_swagger_ui_html |
| from fastapi.templating import Jinja2Templates |
| from fastapi.middleware.cors import CORSMiddleware |
| |
| |
| |
| |
| |
| |
| |
| from socket_routes.status import add_status_schedulers |
| |
| from websocket.manager import websocketmanager |
| |
| from scheduler.scheduler import scheduler |
| |
| |
| |
| scheduler.start() |
| |
| |
| app = FastAPI(docs_url=None) |
| |
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
| |
| templates = Jinja2Templates(directory="templates") |
| |
| |
| app.mount('/static', StaticFiles(directory='static'), name='static') |
| |
| |
| @app.get('/docs', include_in_schema=False) |
| async def custom_swagger_ui_html(): |
| return get_swagger_ui_html( |
| openapi_url=app.openapi_url, |
| title=app.title + " - Swagger UI ", |
| oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, |
| swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js", |
| swagger_css_url="/static/swagger-ui/swagger-ui.css", |
| swagger_favicon_url="/static/swagger-ui/favicon.png" |
| ) |
| |
| |
| |
| @app.on_event("startup") |
| async def start_database(): |
| |
| pass |
| |
| |
| |
| @app.get("/", tags=["Docs"]) |
| async def get_docs(): |
| return RedirectResponse("/docs") |
| |
| |
| |
| @app.get("/jinjia2", response_class=HTMLResponse) |
| async def read_jinjia2(request: Request): |
| return templates.TemplateResponse("index.html", {"request": request, "name": "Alice"}) |
| |
| |
| |
| @app.get("/websocket", response_class=HTMLResponse) |
| async def websocket_test(request: Request): |
| return templates.TemplateResponse("websocket.html", {"request": request}) |
| |
| |
| @app.websocket("/wstest") |
| async def websocket_endpoint(websocket: WebSocket): |
| await websocket.accept() |
| while True: |
| data = await websocket.receive_text() |
| await websocket.send_text(f"send from serve: {data}") |
| |
| |
| |
| @app.websocket("/ws") |
| async def websocket_endpoint(websocket: WebSocket): |
| |
| await websocketmanager.connect(websocket) |
| try: |
| while True: |
| |
| data = await websocket.receive_text() |
| data_dict = eval(data) |
| if 'websocket' in data_dict: |
| await websocket.send_text("pong") |
| else: |
| print('start') |
| await add_status_schedulers(websocket) |
| except WebSocketDisconnect: |
| websocketmanager.disconnect(websocket) |
| await websocketmanager.broadcast(f"Client left the chat") |
E:\song\agv_fastapi_socket_v4_mock\main.py
| import uvicorn |
| |
| if __name__ == '__main__': |
| uvicorn.run('app:app', host="127.0.0.1", port=9000, reload=True) |
E:\song\agv_fastapi_socket_v4_mock\config\config.py
| from typing import Optional |
| |
| from beanie import init_beanie |
| from motor.motor_asyncio import AsyncIOMotorClient |
| from pydantic import BaseSettings |
| |
| from models.student import Student |
| |
| |
| class Settings(BaseSettings): |
| |
| DATABASE_URL: Optional[str] = None |
| |
| class Config: |
| env_file = ".env" |
| orm_mode = True |
| |
| |
| async def initiate_database(): |
| client = AsyncIOMotorClient(Settings().DATABASE_URL) |
| await init_beanie(database=client['agv'], |
| document_models=[Student]) |
E:\song\agv_fastapi_socket_v4_mock\dao\student.py
| from typing import List, Union |
| |
| from beanie import PydanticObjectId |
| |
| from models.student import Student |
| |
| student_collection = Student |
| |
| |
| async def retrieve_students() -> List[Student]: |
| students = await student_collection.all().to_list() |
| return students |
| |
| |
| async def add_student(new_student: Student) -> Student: |
| student = await new_student.create() |
| return student |
| |
| |
| async def retrieve_student(id: PydanticObjectId) -> Student: |
| student = await student_collection.get(id) |
| if student: |
| return student |
| |
| |
| async def delete_student(id: PydanticObjectId) -> bool: |
| student = await student_collection.get(id) |
| if student: |
| await student.delete() |
| return True |
| |
| |
| async def update_student_data(id: PydanticObjectId, data: dict) -> Union[bool, Student]: |
| des_body = {k: v for k, v in data.items() if v is not None} |
| update_query = {"$set": { |
| field: value for field, value in des_body.items() |
| }} |
| student = await student_collection.get(id) |
| if student: |
| await student.update(update_query) |
| return student |
| return False |
E:\song\agv_fastapi_socket_v4_mock\models\student.py
| from typing import Optional, Any |
| |
| from beanie import Document |
| from pydantic import BaseModel, EmailStr |
| |
| |
| class Student(Document): |
| fullname: str |
| email: EmailStr |
| course_of_study: str |
| year: int |
| gpa: float |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "fullname": "Abdulazeez Abdulazeez Adeshina", |
| "email": "abdul@school.com", |
| "course_of_study": "Water resources engineering", |
| "year": 4, |
| "gpa": "3.76" |
| } |
| } |
| |
| |
| class UpdateStudentModel(BaseModel): |
| fullname: Optional[str] |
| email: Optional[EmailStr] |
| course_of_study: Optional[str] |
| year: Optional[int] |
| gpa: Optional[float] |
| |
| class Collection: |
| name = "student" |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "fullname": "Abdulazeez Abdulazeez", |
| "email": "abdul@school.com", |
| "course_of_study": "Water resources and environmental engineering", |
| "year": 4, |
| "gpa": "5.0" |
| } |
| } |
| |
| |
| class Response(BaseModel): |
| status_code: int |
| response_type: str |
| description: str |
| data: Optional[Any] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "status_code": 200, |
| "response_type": "success", |
| "description": "Operation successful", |
| "data": "Sample data" |
| } |
| } |
E:\song\agv_fastapi_socket_v4_mock\routes\audio.py
| import json |
| from fastapi import APIRouter, HTTPException |
| from typing import Optional, Any |
| from pydantic import BaseModel |
| |
| |
| audio_router = APIRouter() |
| |
| |
| class StatusParams(BaseModel): |
| play: int |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "play": 1, |
| } |
| } |
| |
| |
| @audio_router.get("/") |
| async def control_audio(play: int): |
| if play == 1: |
| |
| |
| |
| return 'start' |
| else: |
| |
| |
| return 'cancel' |
E:\song\agv_fastapi_socket_v4_mock\routes\config.py
| from fastapi import APIRouter, HTTPException |
| from typing import Optional, Any |
| from pydantic import BaseModel |
| |
| from tcpsocket.factory import agv_socket_factory |
| |
| from tcpsocket.params import Port |
| |
| |
| config_router = APIRouter() |
| |
| |
| agv_config_socket = agv_socket_factory.create(Port.CONFIG.value) |
| |
| |
| class StatusParams(BaseModel): |
| msg_type: int |
| msg: Optional[dict] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "msg_type": 1000, |
| "msg": {"参数名称": "xx参数"} |
| } |
| } |
| |
| |
| @config_router.get("/", response_description="编号 查询结果") |
| async def get_config(msg_type: int): |
| try: |
| agv_config_socket.send(1, msg_type) |
| res = agv_config_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
| |
| |
| @config_router.post("/", response_description="编号 查询结果") |
| async def post_config(params: StatusParams): |
| try: |
| agv_config_socket.send(1, params.msg_type, params.msg) |
| res = agv_config_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
E:\song\agv_fastapi_socket_v4_mock\routes\control.py
| |
| from fastapi import APIRouter |
| from typing import Optional |
| from pydantic import BaseModel |
| |
| from tcpsocket.factory import agv_socket_factory |
| |
| from tcpsocket.params import Port |
| |
| |
| control_router = APIRouter() |
| |
| |
| agv_control_socket = agv_socket_factory.create(Port.CONTROL.value) |
| |
| |
| class StatusParams(BaseModel): |
| msg_type: int |
| msg: Optional[dict] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "msg_type": 1000, |
| "msg": {"参数名称": "xx参数"} |
| } |
| } |
| |
| |
| @control_router.get("/", response_description="编号 查询结果") |
| async def get_control(): |
| return "地图" |
| |
| |
| @control_router.post("/", response_description="编号 查询结果") |
| async def post_control(params: StatusParams): |
| print(params) |
| return { |
| "map": '地图' |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
E:\song\agv_fastapi_socket_v4_mock\routes\navigate.py
| from fastapi import APIRouter, HTTPException |
| from typing import Optional |
| from pydantic import BaseModel |
| |
| from tcpsocket.factory import agv_socket_factory |
| |
| from tcpsocket.params import Port, Config |
| |
| |
| navigate_router = APIRouter() |
| |
| |
| agv_navigate_socket = agv_socket_factory.create(Port.NAVIGATE.value) |
| |
| |
| class StatusParams(BaseModel): |
| msg_type: int |
| msg: Optional[dict] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "msg_type": 1000, |
| "msg": {"参数名称": "xx参数"} |
| } |
| } |
| |
| |
| @navigate_router.get("/", response_description="编号 查询结果") |
| async def get_navigate(msg_type: int): |
| try: |
| agv_navigate_socket.send(1, msg_type) |
| res = agv_navigate_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
| |
| |
| @navigate_router.post("/", response_description="编号 查询结果") |
| async def post_navigate(params: StatusParams): |
| try: |
| agv_navigate_socket.send(1, params.msg_type, params.msg) |
| res = agv_navigate_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
E:\song\agv_fastapi_socket_v4_mock\routes\other.py
| import json |
| from fastapi import APIRouter, HTTPException |
| from typing import Optional, Any |
| from pydantic import BaseModel |
| |
| from tcpsocket.factory import agv_socket_factory |
| from tcpsocket.params import Status |
| |
| from tcpsocket.params import Port |
| |
| |
| other_router = APIRouter() |
| |
| agv_other_socket = agv_socket_factory.create(Port.OTHER.value) |
| |
| |
| class StatusParams(BaseModel): |
| msg_type: int |
| msg: Optional[Any] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "msg_type": 1000, |
| "msg": {"参数名称": "xx参数"} |
| } |
| } |
| |
| |
| @other_router.get("/") |
| async def get_other(msg_type: int): |
| try: |
| agv_other_socket.send(1, msg_type) |
| res = agv_other_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
| |
| |
| @other_router.post("/", response_description="编号 查询结果") |
| async def post_other(params: StatusParams): |
| try: |
| agv_other_socket.send(1, params.msg_type, params.msg) |
| res = agv_other_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
E:\song\agv_fastapi_socket_v4_mock\routes\scheduler.py
| from fastapi import APIRouter, Body |
| from scheduler.scheduler import scheduler |
| |
| scheduler_router = APIRouter() |
| |
| |
| @scheduler_router.get("/", response_description="Toggle the scheduler") |
| async def toggle_scheduler(flag: str): |
| if flag == 'start': |
| pass |
| elif flag == 'resume': |
| scheduler.resume() |
| elif flag == 'pause': |
| scheduler.pause() |
| return { |
| "status_code": 200, |
| "response_type": "success", |
| "description": "Students data retrieved successfully", |
| "data": "ok" |
| } |
E:\song\agv_fastapi_socket_v4_mock\routes\status.py
| import json |
| from fastapi import APIRouter, HTTPException |
| from typing import Optional, Any |
| from pydantic import BaseModel |
| |
| from tcpsocket.factory import agv_socket_factory |
| |
| from tcpsocket.params import Port |
| |
| |
| status_router = APIRouter() |
| |
| agv_status_socket = agv_socket_factory.create(Port.STATUS.value) |
| |
| |
| class StatusParams(BaseModel): |
| msg_type: int |
| msg: Optional[Any] |
| |
| class Config: |
| schema_extra = { |
| "example": { |
| "msg_type": 1000, |
| "msg": {"参数名称": "xx参数"} |
| } |
| } |
| |
| |
| @status_router.get("/") |
| async def get_status(msg_type: int): |
| try: |
| agv_status_socket.send(1, msg_type) |
| res = agv_status_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
| |
| |
| @status_router.post("/", response_description="编号 查询结果") |
| async def post_status(params: StatusParams): |
| try: |
| agv_status_socket.send(1, params.msg_type, params.msg) |
| res = agv_status_socket.receive() |
| return res |
| except Exception as e: |
| print(e) |
| raise HTTPException( |
| status_code=409, |
| detail="查询编号不正确" |
| ) |
E:\song\agv_fastapi_socket_v4_mock\routes\student.py
| from fastapi import APIRouter, Body |
| |
| from dao.student import * |
| from models.student import * |
| |
| student_router = APIRouter() |
| |
| |
| @student_router.get("/", response_description="Students retrieved", response_model=Response) |
| async def get_students(): |
| return { |
| "status_code": 200, |
| "response_type": "success", |
| "description": "Students data retrieved successfully", |
| "data": "test" |
| } |
E:\song\agv_fastapi_socket_v4_mock\scheduler\scheduler.py
| |
| from apscheduler.schedulers.asyncio import AsyncIOScheduler |
| scheduler = AsyncIOScheduler() |
E:\song\agv_fastapi_socket_v4_mock\socket_routes\status.py
| import json |
| import time |
| import random |
| from websocket.manager import websocketmanager |
| from scheduler.scheduler import scheduler |
| |
| time_start = time.time() |
| |
| |
| |
| |
| async def add_status_schedulers(websocket): |
| scheduler.add_job(job_status_test, 'interval', |
| seconds=2, args=[websocket]) |
| |
| |
| async def job_status_test(websocket): |
| |
| time_end = time.time() |
| |
| bat_temp = format((random.uniform(18, 22)), '.2f') |
| con_temp = format((random.uniform(38, 42)), '.2f') |
| print(int(round((time_end-time_start) * 1000))) |
| await websocketmanager.broadcast_json({ |
| "name": "test", |
| "data": { |
| "battery_temp": bat_temp, |
| "controller_temp": con_temp, |
| "time": int(round((time_end-time_start) * 1000)) |
| } |
| }, websocket) |
E:\song\agv_fastapi_socket_v4_mock\tcpsocket\agvsocket.py
| import json |
| import socket |
| import struct |
| |
| |
| class AgvSocket: |
| __PACK_FMT_STR = '!BBHLH6s' |
| |
| def __init__(self, ip, port): |
| so = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| so.connect((ip, port)) |
| so.settimeout(5) |
| self.so = so |
| |
| def send(self, req_id, msg_type, msg={}): |
| raw_msg = self.__pack_msg(req_id, msg_type, msg) |
| self.so.send(raw_msg) |
| |
| def receive(self): |
| global data |
| data_all = b'' |
| |
| try: |
| data = self.so.recv(16) |
| except socket.timeout: |
| print('timeout') |
| self.so.close() |
| |
| json_data_len = 0 |
| back_req_num = 0 |
| |
| if len(data) < 16: |
| print('pack head error') |
| print(data) |
| self.so.close() |
| else: |
| header = struct.unpack(AgvSocket.__PACK_FMT_STR, data) |
| json_data_len = header[3] |
| back_req_num = header[4] |
| data_all += data |
| data = b'' |
| read_size = 1024 |
| try: |
| while json_data_len > 0: |
| recv = self.so.recv(read_size) |
| data += recv |
| json_data_len -= len(recv) |
| if json_data_len < read_size: |
| read_size = json_data_len |
| data_all += data |
| return json.loads(data_all[16:].decode()) |
| |
| except socket.timeout: |
| print('timeout') |
| |
| def close(self): |
| self.so.close() |
| |
| @staticmethod |
| def __pack_msg(req_id, msg_type, msg={}): |
| msg_len = 0 |
| json_str = json.dumps(msg) |
| |
| if msg != {}: |
| msg_len = len(json_str) |
| |
| raw_msg = struct.pack(AgvSocket.__PACK_FMT_STR, 0x5A, 0x01, req_id, |
| msg_len, msg_type, b'\x00\x00\x00\x00\x00\x00') |
| if msg != {}: |
| raw_msg += bytearray(json_str, 'ascii') |
| |
| return raw_msg |
E:\song\agv_fastapi_socket_v4_mock\tcpsocket\factory.py
| from .params import IP, Port |
| from .agvsocket import AgvSocket |
| |
| |
| class FactoryTcpSocket: |
| def __init__(self, ip: str): |
| self.ip = ip |
| self.socket_dic = {} |
| |
| def create(self, port): |
| pass |
| res = self.socket_dic.get(port) |
| if res is None: |
| agv_socket = AgvSocket(self.ip, port) |
| self.socket_dic[port] = agv_socket |
| return agv_socket |
| else: |
| return res |
| |
| |
| agv_socket_factory = FactoryTcpSocket(IP) |
E:\song\agv_fastapi_socket_v4_mock\tcpsocket\params.py
| from enum import Enum |
| |
| IP = "10.13.32.11" |
| |
| |
| |
| class Port(Enum): |
| STATUS = 19204 |
| CONTROL = 19205 |
| NAVIGATE = 19206 |
| CONFIG = 19207 |
| OTHER = 19210 |
| PUSH = 192301 |
| |
| |
| class Status(Enum): |
| INFO = 1000 |
| RUN = 1002 |
| LOC = 1004 |
| SPEED = 1005 |
| BLOCK = 1006 |
| BATTERY = 1007 |
| MOTOR = 1040 |
| LASER = 1009 |
| AREA = 1011 |
| EMERGENCY = 1012 |
| IO = 1013 |
| IMU = 1014 |
| RFID = 1015 |
| ULTRASONIC = 1016 |
| PGV = 1017 |
| ENCODER = 1018 |
| TASK = 1020 |
| TASK_STATUS_PACKAGE = 1110 |
| RELOC = 1021 |
| LOADMAP = 1022 |
| SLAM = 1025 |
| DISPATCH = 1030 |
| ALARM = 1050 |
| CURRENT_LOCK = 1060 |
| MAP = 1300 |
| STATION = 1301 |
| STARTDMXSCRIPT = 1903 |
| STOPBATTERYSCRIPT = 1904 |
| ALL1 = 1100 |
| ALL2 = 1101 |
| ALL3 = 1102 |
| |
| |
| class Control(Enum): |
| pass |
| |
| |
| class Navigate(Enum): |
| pass |
| |
| |
| class Config(Enum): |
| |
| DOWN_LOAD_MAP = 4011 |
| |
| |
| class Other(Enum): |
| pass |
| |
| |
| class Push(Enum): |
| pass |
E:\song\agv_fastapi_socket_v4_mock\templates\index.html
| <!DOCTYPE html> |
| <html lang="zh"> |
| |
| <head> |
| <meta charset="UTF-8"> |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>jinjia2</title> |
| <link href="{{ url_for('static', path='/index/style.css') }}" rel="stylesheet"> |
| </head> |
| |
| <body> |
| <h1>this is jinja2 template</h1> |
| <h2>{{name}}</h2> |
| |
| </body> |
| |
| </html>``` |
| # `E:\song\agv_fastapi_socket_v4_mock\templates\websocket.html` |
| |
| ```html |
| <!DOCTYPE html> |
| <html> |
| |
| <head> |
| <title>Chat</title> |
| </head> |
| |
| <body> |
| <h1>WebSocket Chat</h1> |
| <form action="" onsubmit="sendMessage(event)"> |
| <input type="text" id="messageText" autocomplete="off" /> |
| <button>Send</button> |
| </form> |
| <ul id='messages'> |
| </ul> |
| <script> |
| |
| const ws = new WebSocket("ws://localhost:9000/ws"); |
| |
| |
| ws.addEventListener('open', (e) => { |
| console.log(`socket 连接成功`) |
| }) |
| |
| ws.addEventListener('error', (e) => { |
| console.log(`socket 连接失败`) |
| }) |
| |
| ws.addEventListener('close', (e) => { |
| console.log(`socket 连接断开`) |
| }) |
| |
| |
| ws.onmessage = function (event) { |
| receMessage(event.data) |
| }; |
| |
| |
| function receMessage(msg) { |
| var messages = document.getElementById('messages') |
| var message = document.createElement('li') |
| var content = document.createTextNode(msg) |
| message.appendChild(content) |
| messages.appendChild(message) |
| } |
| |
| |
| function sendMessage(event) { |
| var input = document.getElementById("messageText") |
| ws.send(input.value) |
| input.value = '' |
| event.preventDefault() |
| } |
| </script> |
| </body> |
| |
| </html>``` |
| # `E:\song\agv_fastapi_socket_v4_mock\websocket\dispatch.py` |
| |
| ```py |
| from socket_routes.status import dispatch_status |
| |
| |
| # 分发任务 |
| async def dispatch_socket(msg_type, msg, websocket): |
| # 查询机器人状态 |
| if msg_type >= 1000 and msg_type <= 1999: |
| print("状态api") |
| await dispatch_status(msg_type, msg, websocket) |
| elif msg_type >= 2000 and msg_type <= 2999: |
| print("控制api") |
| elif msg_type >= 3000 and msg_type <= 3999: |
| print("导航api") |
| elif msg_type >= 4000 and msg_type <= 4999: |
| print("配置api") |
| elif msg_type >= 6000 and msg_type <= 6999: |
| print("其他api") |
| elif msg_type == 9300 or msg_type == 19301: |
| print("推送api") |
| else: |
| print("暂无此api") |
E:\song\agv_fastapi_socket_v4_mock\websocket\manager.py
| from typing import List |
| from fastapi.websockets import WebSocket |
| |
| |
| class ConnectionManager: |
| def __init__(self): |
| self.active_connections: List[WebSocket] = [] |
| |
| async def connect(self, websocket: WebSocket): |
| await websocket.accept() |
| self.active_connections.append(websocket) |
| |
| def disconnect(self, websocket: WebSocket): |
| self.active_connections.remove(websocket) |
| |
| |
| async def send_personal_message(self, message: str, websocket: WebSocket): |
| await websocket.send_text(message) |
| |
| async def send_personal_json(self, message: str, websocket: WebSocket): |
| await websocket.send_json(message) |
| |
| async def broadcast_json(self, message: str, websocket: WebSocket): |
| for connection in self.active_connections: |
| await connection.send_json(message) |
| |
| |
| |
| async def broadcast(self, message: str): |
| for connection in self.active_connections: |
| await connection.send_text(message) |
| |
| |
| websocketmanager = ConnectionManager() |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战