第09章-二次开发进阶
第09章:二次开发进阶
9.1 深入OCP开发
9.1.1 OCCT拓扑结构
理解OpenCASCADE的拓扑结构对于高级开发至关重要:
Compound (复合体)
└── CompSolid (复合实体)
└── Solid (实体)
└── Shell (壳)
└── Face (面)
└── Wire (线框)
└── Edge (边)
└── Vertex (顶点)
import cadquery as cq
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import (
TopAbs_COMPOUND, TopAbs_SOLID, TopAbs_SHELL,
TopAbs_FACE, TopAbs_WIRE, TopAbs_EDGE, TopAbs_VERTEX
)
def explore_topology(shape):
"""探索OCCT拓扑结构"""
occ_shape = shape.val().wrapped
topology_types = [
(TopAbs_COMPOUND, "Compound"),
(TopAbs_SOLID, "Solid"),
(TopAbs_SHELL, "Shell"),
(TopAbs_FACE, "Face"),
(TopAbs_WIRE, "Wire"),
(TopAbs_EDGE, "Edge"),
(TopAbs_VERTEX, "Vertex"),
]
print("拓扑结构分析:")
for topo_type, name in topology_types:
explorer = TopExp_Explorer(occ_shape, topo_type)
count = 0
while explorer.More():
count += 1
explorer.Next()
if count > 0:
print(f" {name}: {count}")
# 使用
model = cq.Workplane("XY").box(30, 20, 10)
explore_topology(model)
9.1.2 几何曲线操作
import cadquery as cq
from OCP.Geom import Geom_BSplineCurve
from OCP.TColgp import TColgp_Array1OfPnt
from OCP.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger
from OCP.gp import gp_Pnt
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeWire
def make_bspline_edge(control_points, degree=3):
"""
创建B样条曲线边
Args:
control_points: 控制点列表 [(x, y, z), ...]
degree: 曲线阶数
"""
n = len(control_points)
# 创建控制点数组
poles = TColgp_Array1OfPnt(1, n)
for i, (x, y, z) in enumerate(control_points):
poles.SetValue(i + 1, gp_Pnt(x, y, z))
# 创建节点向量(均匀)
num_knots = n - degree + 1
knots = TColStd_Array1OfReal(1, num_knots)
for i in range(num_knots):
knots.SetValue(i + 1, float(i))
# 创建重复度数组
mults = TColStd_Array1OfInteger(1, num_knots)
for i in range(num_knots):
if i == 0 or i == num_knots - 1:
mults.SetValue(i + 1, degree + 1)
else:
mults.SetValue(i + 1, 1)
# 创建B样条曲线
curve = Geom_BSplineCurve(poles, knots, mults, degree)
# 转换为边
edge_builder = BRepBuilderAPI_MakeEdge(curve)
edge = edge_builder.Edge()
return cq.Edge(edge)
# 使用
control_pts = [(0, 0, 0), (10, 20, 0), (30, 10, 0), (50, 30, 0), (70, 0, 0)]
edge = make_bspline_edge(control_pts)
9.1.3 曲面操作
import cadquery as cq
from OCP.Geom import Geom_BSplineSurface
from OCP.TColgp import TColgp_Array2OfPnt
from OCP.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger
from OCP.gp import gp_Pnt
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
import math
def make_bspline_surface(z_func, x_range, y_range, x_points=10, y_points=10):
"""
从函数创建B样条曲面
Args:
z_func: z = f(x, y) 函数
x_range: (x_min, x_max)
y_range: (y_min, y_max)
x_points: X方向点数
y_points: Y方向点数
"""
# 创建控制点网格
poles = TColgp_Array2OfPnt(1, x_points, 1, y_points)
x_min, x_max = x_range
y_min, y_max = y_range
for i in range(x_points):
for j in range(y_points):
x = x_min + (x_max - x_min) * i / (x_points - 1)
y = y_min + (y_max - y_min) * j / (y_points - 1)
z = z_func(x, y)
poles.SetValue(i + 1, j + 1, gp_Pnt(x, y, z))
# 创建节点向量
degree_u = min(3, x_points - 1)
degree_v = min(3, y_points - 1)
# 简化:使用均匀节点
u_knots = TColStd_Array1OfReal(1, 2)
v_knots = TColStd_Array1OfReal(1, 2)
u_knots.SetValue(1, 0.0)
u_knots.SetValue(2, 1.0)
v_knots.SetValue(1, 0.0)
v_knots.SetValue(2, 1.0)
u_mults = TColStd_Array1OfInteger(1, 2)
v_mults = TColStd_Array1OfInteger(1, 2)
u_mults.SetValue(1, x_points)
u_mults.SetValue(2, x_points)
v_mults.SetValue(1, y_points)
v_mults.SetValue(2, y_points)
# 创建B样条曲面
surface = Geom_BSplineSurface(
poles, u_knots, v_knots, u_mults, v_mults,
degree_u, degree_v
)
# 转换为面
face_builder = BRepBuilderAPI_MakeFace(surface, 1e-6)
face = face_builder.Face()
return cq.Face(face)
# 使用示例
def wave_function(x, y):
return 10 * math.sin(x * 0.2) * math.cos(y * 0.2)
# face = make_bspline_surface(wave_function, (-20, 20), (-20, 20))
9.1.4 高级布尔运算
import cadquery as cq
from OCP.BRepAlgoAPI import BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut, BRepAlgoAPI_Common
from OCP.BRepAlgoAPI import BRepAlgoAPI_Section
def advanced_boolean(shape1, shape2, operation="fuse"):
"""
高级布尔运算,提供更多控制
Args:
shape1, shape2: CadQuery形状
operation: "fuse", "cut", "common", "section"
"""
s1 = shape1.val().wrapped
s2 = shape2.val().wrapped
if operation == "fuse":
builder = BRepAlgoAPI_Fuse(s1, s2)
elif operation == "cut":
builder = BRepAlgoAPI_Cut(s1, s2)
elif operation == "common":
builder = BRepAlgoAPI_Common(s1, s2)
elif operation == "section":
builder = BRepAlgoAPI_Section(s1, s2)
else:
raise ValueError(f"未知操作: {operation}")
# 执行运算
builder.Build()
if not builder.IsDone():
raise RuntimeError("布尔运算失败")
result_shape = builder.Shape()
return cq.Workplane("XY").add(cq.Shape.cast(result_shape))
# 使用
box = cq.Workplane("XY").box(30, 30, 30)
sphere = cq.Workplane("XY").sphere(20)
fused = advanced_boolean(box, sphere, "fuse")
cut = advanced_boolean(box, sphere, "cut")
common = advanced_boolean(box, sphere, "common")
9.2 自定义建模函数库
9.2.1 标准件库
import cadquery as cq
import math
class StandardParts:
"""标准件库"""
@staticmethod
def hex_bolt(diameter, length, head_height=None, thread_length=None):
"""
六角螺栓
Args:
diameter: 公称直径(如M8对应8)
length: 螺栓总长度
head_height: 头部高度(默认为0.7*diameter)
thread_length: 螺纹长度(默认为全螺纹)
"""
if head_height is None:
head_height = diameter * 0.7
if thread_length is None:
thread_length = length
head_width = diameter * 1.7 # 对边距离
return (
cq.Workplane("XY")
.polygon(6, head_width * 2 / math.sqrt(3)) # 六边形
.extrude(head_height)
.faces("<Z")
.workplane()
.circle(diameter / 2)
.extrude(-length)
.edges(">Z")
.chamfer(head_height * 0.1)
.edges("<Z")
.chamfer(diameter * 0.1)
)
@staticmethod
def hex_nut(diameter, height=None):
"""
六角螺母
Args:
diameter: 公称直径
height: 高度(默认为0.8*diameter)
"""
if height is None:
height = diameter * 0.8
width = diameter * 1.7
return (
cq.Workplane("XY")
.polygon(6, width * 2 / math.sqrt(3))
.extrude(height)
.faces(">Z")
.workplane()
.hole(diameter)
.edges()
.chamfer(height * 0.1)
)
@staticmethod
def washer(inner_diameter, outer_diameter, thickness):
"""
垫圈
Args:
inner_diameter: 内径
outer_diameter: 外径
thickness: 厚度
"""
return (
cq.Workplane("XY")
.circle(outer_diameter / 2)
.circle(inner_diameter / 2)
.extrude(thickness)
)
@staticmethod
def bearing(inner_d, outer_d, width):
"""
简化轴承
Args:
inner_d: 内径
outer_d: 外径
width: 宽度
"""
return (
cq.Workplane("XY")
.circle(outer_d / 2)
.circle(inner_d / 2)
.extrude(width)
.faces(">Z or <Z")
.shell(-width * 0.1)
)
# 使用
bolt = StandardParts.hex_bolt(8, 30)
nut = StandardParts.hex_nut(8)
washer = StandardParts.washer(8.4, 16, 1.5)
cq.exporters.export(bolt, "m8_bolt.step")
cq.exporters.export(nut, "m8_nut.step")
cq.exporters.export(washer, "washer.step")
9.2.2 机械零件库
import cadquery as cq
import math
class MechanicalParts:
"""机械零件库"""
@staticmethod
def spur_gear(module, teeth, width, bore, pressure_angle=20):
"""
简化直齿轮
Args:
module: 模数
teeth: 齿数
width: 齿宽
bore: 轴孔直径
pressure_angle: 压力角
"""
pitch_radius = module * teeth / 2
addendum = module
dedendum = 1.25 * module
outer_radius = pitch_radius + addendum
root_radius = pitch_radius - dedendum
# 简化为圆柱体(实际齿轮需要更复杂的几何)
return (
cq.Workplane("XY")
.circle(outer_radius)
.extrude(width)
.faces(">Z")
.workplane()
.hole(bore)
# 添加轮辐(可选)
.faces(">Z or <Z")
.shell(-width * 0.2)
)
@staticmethod
def shaft(diameter, length, keyway=None, keyway_depth=None):
"""
轴
Args:
diameter: 轴径
length: 长度
keyway: 键槽宽度(可选)
keyway_depth: 键槽深度(可选)
"""
result = cq.Workplane("XY").cylinder(length, diameter / 2)
if keyway and keyway_depth:
result = (
result
.faces(">Z")
.workplane()
.transformed(offset=(diameter/2 - keyway_depth/2, 0, 0))
.rect(keyway_depth, keyway)
.cutBlind(-length * 0.6)
)
# 倒角
result = result.faces(">Z or <Z").edges().chamfer(diameter * 0.05)
return result
@staticmethod
def pulley(outer_diameter, bore, width, groove_depth=5, num_grooves=1):
"""
皮带轮
Args:
outer_diameter: 外径
bore: 轴孔直径
width: 宽度
groove_depth: 槽深
num_grooves: 槽数
"""
result = (
cq.Workplane("XY")
.circle(outer_diameter / 2)
.extrude(width)
.faces(">Z")
.workplane()
.hole(bore)
)
# 添加V型槽
groove_width = width / (num_grooves + 1)
for i in range(num_grooves):
offset = groove_width * (i + 1) - width / 2
result = (
result
.faces(">Z")
.workplane(offset=offset)
.transformed(rotate=(90, 0, 0))
.circle(outer_diameter / 2 - groove_depth)
.extrude(groove_width * 0.3, both=True)
)
return result
@staticmethod
def flange(outer_d, inner_d, thickness, bolt_circle_d,
bolt_hole_d, num_bolts, hub_d=None, hub_h=0):
"""
法兰盘
Args:
outer_d: 外径
inner_d: 内径
thickness: 厚度
bolt_circle_d: 螺栓圆直径
bolt_hole_d: 螺栓孔直径
num_bolts: 螺栓数量
hub_d: 凸台直径
hub_h: 凸台高度
"""
result = (
cq.Workplane("XY")
.circle(outer_d / 2)
.extrude(thickness)
)
# 凸台
if hub_d and hub_h > 0:
result = (
result
.faces(">Z")
.workplane()
.circle(hub_d / 2)
.extrude(hub_h)
)
# 中心孔
result = result.faces(">Z").workplane().hole(inner_d)
# 螺栓孔
result = (
result
.faces("<Z")
.workplane(invert=True)
.polarArray(bolt_circle_d / 2, 0, 360, num_bolts)
.hole(bolt_hole_d)
)
return result
# 使用
gear = MechanicalParts.spur_gear(2, 30, 15, 10)
shaft = MechanicalParts.shaft(20, 100, keyway=6, keyway_depth=3)
flange = MechanicalParts.flange(100, 50, 15, 75, 10, 6, hub_d=60, hub_h=20)
cq.exporters.export(gear, "gear.step")
cq.exporters.export(shaft, "shaft.step")
cq.exporters.export(flange, "flange.step")
9.2.3 电子外壳库
import cadquery as cq
class ElectronicEnclosures:
"""电子外壳库"""
@staticmethod
def simple_box(length, width, height, wall_thickness=2, corner_radius=3):
"""简单盒子外壳"""
return (
cq.Workplane("XY")
.box(length, width, height)
.edges("|Z")
.fillet(corner_radius)
.faces(">Z")
.shell(-wall_thickness)
)
@staticmethod
def pcb_enclosure(pcb_length, pcb_width, pcb_height,
wall=2, clearance=1, standoff_height=5,
standoff_diameter=6, hole_diameter=2.5):
"""
PCB外壳
Args:
pcb_length, pcb_width: PCB尺寸
pcb_height: PCB上最高元件高度
wall: 壁厚
clearance: PCB周边间隙
standoff_height: 支撑柱高度
standoff_diameter: 支撑柱直径
hole_diameter: 安装孔直径
"""
inner_l = pcb_length + clearance * 2
inner_w = pcb_width + clearance * 2
inner_h = standoff_height + pcb_height + clearance
outer_l = inner_l + wall * 2
outer_w = inner_w + wall * 2
outer_h = inner_h + wall
# 创建底壳
base = (
cq.Workplane("XY")
.box(outer_l, outer_w, outer_h)
.edges("|Z")
.fillet(wall)
.faces(">Z")
.shell(-wall)
)
# 添加支撑柱
pcb_offset_x = (pcb_length - standoff_diameter) / 2
pcb_offset_y = (pcb_width - standoff_diameter) / 2
standoff_positions = [
(-pcb_offset_x, -pcb_offset_y),
(-pcb_offset_x, pcb_offset_y),
(pcb_offset_x, -pcb_offset_y),
(pcb_offset_x, pcb_offset_y),
]
base = (
base
.faces("<Z[1]") # 内底面
.workplane()
.pushPoints(standoff_positions)
.circle(standoff_diameter / 2)
.extrude(standoff_height)
.faces(">Z")
.workplane()
.pushPoints(standoff_positions)
.hole(hole_diameter, standoff_height)
)
return base
@staticmethod
def vented_enclosure(length, width, height, wall=2,
vent_width=2, vent_spacing=4, vent_length=20):
"""
带散热孔的外壳
"""
# 基础外壳
result = (
cq.Workplane("XY")
.box(length, width, height)
.edges("|Z")
.fillet(wall * 1.5)
.faces(">Z")
.shell(-wall)
)
# 在侧面添加散热孔
num_vents = int((height - 10) / (vent_width + vent_spacing))
for i in range(num_vents):
z_offset = 5 + i * (vent_width + vent_spacing)
result = (
result
.faces(">X")
.workplane()
.center(0, z_offset - height/2 + wall)
.rect(vent_length, vent_width)
.cutBlind(-wall)
)
return result
# 使用
simple_box = ElectronicEnclosures.simple_box(100, 60, 30)
pcb_case = ElectronicEnclosures.pcb_enclosure(80, 50, 20)
vented = ElectronicEnclosures.vented_enclosure(120, 80, 50)
cq.exporters.export(pcb_case, "pcb_enclosure.step")
9.3 Web服务集成
9.3.1 Flask REST API
import cadquery as cq
from flask import Flask, request, send_file, jsonify
import io
import tempfile
import os
app = Flask(__name__)
@app.route('/api/generate_box', methods=['POST'])
def generate_box():
"""生成盒子并返回STEP文件"""
data = request.json
length = data.get('length', 30)
width = data.get('width', 20)
height = data.get('height', 10)
fillet = data.get('fillet', 0)
# 创建模型
result = cq.Workplane("XY").box(length, width, height)
if fillet > 0:
result = result.edges().fillet(fillet)
# 导出到临时文件
with tempfile.NamedTemporaryFile(suffix='.step', delete=False) as f:
cq.exporters.export(result, f.name)
return send_file(f.name, as_attachment=True,
download_name='box.step')
@app.route('/api/model_info', methods=['POST'])
def model_info():
"""返回模型信息"""
data = request.json
length = data.get('length', 30)
width = data.get('width', 20)
height = data.get('height', 10)
result = cq.Workplane("XY").box(length, width, height)
bb = result.val().BoundingBox()
return jsonify({
'volume': result.val().Volume(),
'bounding_box': {
'min': [bb.xmin, bb.ymin, bb.zmin],
'max': [bb.xmax, bb.ymax, bb.zmax]
},
'faces_count': len(result.faces().vals()),
'edges_count': len(result.edges().vals())
})
# if __name__ == '__main__':
# app.run(debug=True)
9.3.2 FastAPI集成
import cadquery as cq
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel
import tempfile
import os
app = FastAPI(title="CadQuery API")
class BoxParams(BaseModel):
length: float = 30
width: float = 20
height: float = 10
fillet_radius: float = 0
class CylinderParams(BaseModel):
radius: float = 10
height: float = 30
hollow: bool = False
wall_thickness: float = 2
@app.post("/generate/box")
async def generate_box(params: BoxParams):
"""生成参数化盒子"""
try:
result = cq.Workplane("XY").box(params.length, params.width, params.height)
if params.fillet_radius > 0:
max_fillet = min(params.length, params.width, params.height) / 2 * 0.9
fillet = min(params.fillet_radius, max_fillet)
result = result.edges().fillet(fillet)
# 导出
with tempfile.NamedTemporaryFile(suffix='.step', delete=False) as f:
cq.exporters.export(result, f.name)
return FileResponse(f.name, filename='box.step',
media_type='application/octet-stream')
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/generate/cylinder")
async def generate_cylinder(params: CylinderParams):
"""生成参数化圆柱"""
try:
result = cq.Workplane("XY").cylinder(params.height, params.radius)
if params.hollow:
inner_radius = params.radius - params.wall_thickness
if inner_radius > 0:
result = result.faces(">Z").workplane().hole(inner_radius * 2)
with tempfile.NamedTemporaryFile(suffix='.step', delete=False) as f:
cq.exporters.export(result, f.name)
return FileResponse(f.name, filename='cylinder.step',
media_type='application/octet-stream')
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 运行: uvicorn main:app --reload
9.3.3 批量处理服务
import cadquery as cq
import json
import os
from concurrent.futures import ProcessPoolExecutor
def generate_part(config):
"""生成单个零件"""
part_type = config.get('type')
params = config.get('params', {})
output = config.get('output', 'part.step')
if part_type == 'box':
result = cq.Workplane("XY").box(
params.get('length', 30),
params.get('width', 20),
params.get('height', 10)
)
elif part_type == 'cylinder':
result = cq.Workplane("XY").cylinder(
params.get('height', 30),
params.get('radius', 10)
)
elif part_type == 'sphere':
result = cq.Workplane("XY").sphere(
params.get('radius', 15)
)
else:
raise ValueError(f"未知类型: {part_type}")
cq.exporters.export(result, output)
return output
def batch_generate(config_file, output_dir='output'):
"""批量生成零件"""
os.makedirs(output_dir, exist_ok=True)
with open(config_file, 'r') as f:
configs = json.load(f)
results = []
for i, config in enumerate(configs):
config['output'] = os.path.join(output_dir, f"part_{i}.step")
result = generate_part(config)
results.append(result)
print(f"生成: {result}")
return results
# 配置文件示例 (batch_config.json):
# [
# {"type": "box", "params": {"length": 30, "width": 20, "height": 10}},
# {"type": "cylinder", "params": {"radius": 15, "height": 40}},
# {"type": "sphere", "params": {"radius": 20}}
# ]
9.4 与其他工具集成
9.4.1 FreeCAD集成
# 在FreeCAD中使用CadQuery
# 需要安装cadquery-freecad-module
import cadquery as cq
def export_for_freecad(model, filename):
"""导出为FreeCAD可读格式"""
cq.exporters.export(model, filename + ".step")
cq.exporters.export(model, filename + ".brep")
# 示例
model = cq.Workplane("XY").box(30, 20, 10)
export_for_freecad(model, "for_freecad")
9.4.2 Blender集成
import cadquery as cq
def export_for_blender(model, filename, format='stl'):
"""
导出为Blender可导入的格式
Args:
model: CadQuery模型
filename: 输出文件名
format: 'stl' 或 'gltf'
"""
if format == 'stl':
cq.exporters.export(model, filename + ".stl", tolerance=0.01)
elif format == 'gltf':
cq.exporters.export(model, filename + ".gltf")
else:
raise ValueError(f"不支持的格式: {format}")
# 使用
model = cq.Workplane("XY").box(30, 20, 10)
export_for_blender(model, "for_blender", format='gltf')
9.4.3 3D打印切片软件集成
import cadquery as cq
import subprocess
import os
def prepare_for_printing(model, filename, layer_height=0.2,
infill=20, slicer='cura'):
"""
准备3D打印文件
Args:
model: CadQuery模型
filename: 输出文件名
layer_height: 层高
infill: 填充率
slicer: 切片软件 ('cura', 'prusa')
"""
# 导出优化的STL
stl_file = filename + ".stl"
tolerance = layer_height / 2
cq.exporters.export(model, stl_file, tolerance=tolerance)
print(f"STL文件已生成: {stl_file}")
print(f"建议打印参数:")
print(f" 层高: {layer_height}mm")
print(f" 填充: {infill}%")
# 可选:调用切片软件
# if slicer == 'cura':
# subprocess.run(['cura', stl_file])
# 使用
model = cq.Workplane("XY").box(30, 20, 10)
prepare_for_printing(model, "print_ready", layer_height=0.2, infill=20)
9.5 性能优化高级技巧
9.5.1 并行处理
import cadquery as cq
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import multiprocessing
def create_part(params):
"""创建单个零件(独立函数,可序列化)"""
name, length, width, height = params
result = cq.Workplane("XY").box(length, width, height)
cq.exporters.export(result, f"{name}.step")
return name
def parallel_generation(parts_params, max_workers=None):
"""
并行生成多个零件
Args:
parts_params: [(name, l, w, h), ...]
max_workers: 最大工作进程数
"""
if max_workers is None:
max_workers = multiprocessing.cpu_count()
with ProcessPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(create_part, parts_params))
return results
# 使用
parts = [
("part_1", 30, 20, 10),
("part_2", 40, 30, 15),
("part_3", 50, 40, 20),
("part_4", 60, 50, 25),
]
# results = parallel_generation(parts)
9.5.2 内存优化
import cadquery as cq
import gc
def memory_efficient_batch(configs, batch_size=10):
"""
内存高效的批量处理
Args:
configs: 配置列表
batch_size: 批次大小
"""
for i in range(0, len(configs), batch_size):
batch = configs[i:i + batch_size]
for config in batch:
# 处理
model = cq.Workplane("XY").box(
config['length'],
config['width'],
config['height']
)
cq.exporters.export(model, config['output'])
# 清理
del model
# 批次结束后强制垃圾回收
gc.collect()
print(f"完成批次 {i // batch_size + 1}")
9.6 本章小结
本章介绍了CadQuery二次开发的进阶内容:
-
深入OCP开发
- OCCT拓扑结构
- 曲线曲面操作
- 高级布尔运算
-
自定义建模函数库
- 标准件库
- 机械零件库
- 电子外壳库
-
Web服务集成
- Flask/FastAPI API
- 批量处理服务
-
工具集成
- FreeCAD集成
- Blender集成
- 3D打印工作流
-
性能优化
- 并行处理
- 内存优化
通过本章学习,您应该能够:
- 使用OCP进行底层几何操作
- 创建可复用的零件库
- 构建Web服务
- 集成到现有工作流程
- 优化大规模处理性能
下一章将介绍实战案例,展示CadQuery在实际项目中的应用。

浙公网安备 33010602011771号