`
lbxhappy
  • 浏览: 307414 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

model、view、事务详解

阅读更多
与模型的对象关系映射

如前所述,Django 支持与模型的对象关系映射,其中每个模型映射到单个数据库表,并表示子类为 django.db.models.Model 标准类的 Python 类。

以下示例说明如何针对现有表定义模型。在此示例中,您将使用 HR 的 employees 表,仅针对此表的选定字段定义模型字段。在 myproj/myapp 目录中打开 models.py 文件并进行编辑,添加 employees 类,如下所示:

from django.db import models


# Create your models here.

class employees(models.Model):
    employee_id = models.IntegerField(primary_key=True)
    first_name = models.CharField(max_length=20, null = True)
    last_name = models.CharField(max_length=25)
    email = models.CharField(max_length=25)
    class Meta:
         db_table = "employees"

请注意,此处使用了一些模型字段选项。通过设置 primary_key = True,可显式指定此字段是模型的主键。max_length 是 CharField 这一字符串字段所需的参数。如果将可选的 null 参数设置为 True,即表示告诉 Django 将空值作为 NULL 保存到数据库中。默认情况下,此参数设置为 False。要查看字段选项和字段类型的完整列表,您可以参阅 Django 文档中的 Model 字段参考页面。

在上述示例中,另一个需要注意的事项是类 Meta 的使用,您可以通过此类为模型提供元数据选项。在此特定示例中,您使用 db_table 选项显式指定模型要映射到的表的名称。实际上,默认情况下,Django 假设表的名称由模型类的名称和应用程序名称组成(通过下划线 (_) 符号分隔)。因此,在此特定示例中,Django 将查找名为 myapp_employees 的表。当然,db_table 并不是您可用于模型内部类 Meta 的唯一选项。您可以在 Django 文档的 Model Meta 选项页面上查看可用的 Meta 选项列表。

此处讨论的示例很简单,因为它仅显示到单个数据库表的映射。但实际上,您通常必须处理一组通过外键约束相互关联的基础数据库表。为了解决此问题,Django 提供了 ForeignKey 字段类型,可让您定义表示多对一关系的模型字段。

幸运的是,HR 演示模式仅包含一组通过外键约束相互关联的表。例如,您可能选择 departments 表(其 manager_id 字段是 employees 表中 employee_id 的外键),并定义以下模型(将其添加到 models.py 文件中):

class departments(models.Model):
    department_id = models.IntegerField(primary_key=True)
    department_name = models.CharField(max_length=30)
    manager = models.ForeignKey(employees, null = True)
    class Meta:
         db_table = "departments"

看一下上述模型定义,您可能已经注意到,它针对外键模型字段使用名称 manager,而不是实际在 departments 表中使用的 manager_id。实际上,在模型中使用的 manager 字段引用相应的 employees 对象,而非此对象的 employee_id 字段。当外键字段名称要引用基础表中的相应列时,Django 会隐式地将 _id 追加到此名称中。然而,在某些情况下,表中外键字段的名称结尾可能不包含 _id。如果是这种情况,您可以使用外键模型字段的 db_column 参数显式指定表列名称。虽然在此特定示例中无需如此,但您可以使用以下语法在 departments 表中显式指定外键列的名称:

manager = models.ForeignKey(employees, db_column = 'manager_id', null = True)

除了上述通过 ForeignKey 定义的多对一关系之外,Django 还支持一对一和多对多关系,您可以在模型中分别通过 OneToOneField 和 ManyToManyField 字段来定义。

使用数据库抽象 API

完成模型构建之后,您可以继续下一环节,构建将使用这些模型的视图。您可以在此使用 Django 数据库抽象 API 创建、检索、更新以及删除映射到基础数据库中的对象的 Python 对象(模型)。

让我们借助在上一部分中创建的模型,创建一个将从 employees 和 departments 数据库表中获取数据的简单视图。在 myproj/myapp 目录中,打开 views.py 文件并进行编辑,如下所示:

# Create your views here.


from django.template import Context, loader
from django.http import HttpResponse
from myproj.myapp.models import employees, departments


def index(request):
    department_list = departments.objects.exclude(manager__employee_id__exact = None).order_by('manager__employee_id')
    tmpl = loader.get_template("index.html")
    cont = Context({'departments': department_list})
    return HttpResponse(tmpl.render(cont))

在此,您添加了一个简单视图(实际上是一个名为 index 的函数),它将填充部门列表 (department_list),其中仅包括那些具有经理的部门(这意味着,必须填写基础表中的 manager_id 字段)。然后,该视图加载 index.html 模板(将在“创建表示层”部分中讨论),并向其传递 department_list 以便呈现。最后,向 Django 返回包含 HttpResponse 对象的已呈现页面。

在此,最有趣的部分是从 departments 模型中获取数据。请注意 departments 模型类的 objects 属性的用法。通过此属性,您可以访问 models.Manager 对象(不要将它与 departments 模型中定义的 manager 字段混淆)。models.Manager 对象会附加到模型对象中,提供可让您在模型的基础表中查询数据的方法。这些方法将返回一个 QuerySet 对象,其中包含以模型对象的形式检索的数据库数据。QuerySet 对象还提供一组有用的方法,允许您进一步处理返回的模型对象。需要注意的是,一些 QuerySet 方法会返回新的 QuerySet 对象,而其他方法仅对现有 QuerySet 进行求值,然后根据此求值返回一些信息。可以在 Django 文档的 QuerySet API 参考页面上找到完整的 QuerySet 方法列表。

在此讨论的示例中,models.Manager 的 exclude 方法用于将检索的 departments 对象仅限于那些具有相关 employees 对象的对象。然后,QuerySet 的 order_by 方法返回按 departments.manager.employee_id 字段排序的新 QuerySet 对象。它类似于以下 Oracle SQL 查询:

SELECT * FROM departments WHERE manager_id IS NOT NULL ORDER BY manager_id;

除了此处使用的 exclude 方法之外,您还可以通过 objects 属性调用其他三种方法来查询模型数据。清单如下:

    * all — 返回包含所有从模型基础表中获取的模型对象的 QuerySet。
    * filter — 返回仅包含那些与指定条件匹配的模型对象的 QuerySet。
    * exclude — 返回包含与指定条件不匹配的模型对象的 QuerySet。
    * get — 返回与指定条件匹配的单个模型对象。

由于 employees HR 表中的员工 ID 从 100 开始,因此在上一示例中,您可以用 filter 替代 exclude 以便使用数据填充 department_list,如下所示:

department_list = departments.objects.filter(manager__employee_id__gte = 100).order_by('manager__employee_id')

与原始版本一样,要考虑到在 departments 和 employees 模型之间定义的多对一关系(之讨论过),在此情况下,SQL 对应物可能基于 departments 表查询,如下所示:

SELECT * FROM departments WHERE manager_id >= 100 ORDER BY manager_id;

此处讨论的所有模型查询方法(all 方法除外)都采用查找参数以缩小检索模型对象的结果集。因此,在上述示例中,您结合使用以下查找参数和 filter 方法来查询 departments 对象:

manager__employee_id__gte = 100

您可能已经猜到,上述查找参数适合以下模式:field__subfield__lookuptype=value,其中参数由双下划线分隔的关键字组成。在此,manager 是引用 employees 对象的 departments 模型的字段。而 employee_id 是 employees 模型的字段。gte 是表示大于或等于的标准字段查找。可以在此示例中使用上述组合,因为 departments 和 employees 模型通过多对一关系关联。然而,当查询非相关数据时,您应使用以下模式:field__lookuptype=value。例如,为了获得 employee_id 为 100 的 employees 对象,您可以按如下方式查询 employees 模型:

emp = employees.objects.get(employee_id__exact = 100)

实际上,您可能需要发出包含复杂 select 列表和/或复杂 WHERE 子句的查询。例如,您要根据以下查询获得 employees 对象:

SELECT employees.*, (email||'@company.com') as email_address FROM employees;

您如何告诉 Django 向依赖 employees 模型的查询的 select 列表中添加另一个字段?这就是 QuerySet 方法 extra 能够派上用场的地方。

employee_list = employees.objects.all().extra(select={'email_address': "(email||'@company.com')"})

结果,Django 将自动向此处检索的每个 employees 对象添加 extra 属性 email_address。

当然,您不仅可以使用 extra 方法增强 QuerySet 隐式生成的查询的 select 列表。您还可以指定显式 WHERE 子句并向查询的 FROM 子句添加表,以及提供要绑定到 WHERE 子句中指定的相应占位符的动态参数。以下是您可以传递到 extra 方法的参数列表:

    * select — 将额外字段添加到 QuerySet 隐式生成的查询的 select 列表
    * where — 在 QuerySet 查询中指定显式 WHERE 子句
    * tables — 在 QuerySet 查询的 from 列表中包括其他表
    * order_by — 按照通过 select 或 tables 参数添加的字段对 QuerySet 进行排序
    * params — 传入将安全绑定到 WHERE 子句中指定的占位符的动态参数

到目前为止,您已了解说明如何使用 Django 数据库抽象 API 查询基础数据库数据的示例。然而,除了查询之外,您还可以使用 API 创建、更新以及删除数据库数据。

以下代码段说明如何使用此处讨论的 employees 和 departments 模型在 departments 数据库表中创建新的记录:

emp = employees.objects.get(employee_id__exact=100)
new_dept = departments(department_id = 1000, department_name = 'test', manager = emp)
new_dept.save()

结果,应该在 departments 表中显示新行。需要注意的是,model.save 方法还可用于更新现有行:

dept = departments.objects.get(department_id__exact=1000)
dept.department_name = 'new name'
dept.save()

最后,要删除行,请使用 model.delete:

dept = departments.objects.get(department_id__exact=1000)
dept.delete()

事务管理

默认情况下,Django 使用自动提交事务模式。这意味着,它可立即提交通过数据更改模型方法(例如上一部分中的 model.save 和 model.delete)进行的更改。但是,您可以使用 django.db.transaction 模块提供的事务修饰程序,针对特定视图函数更改此默认行为。您具有以下三个选项:

    * @transaction.autocommit(默认)
    * @transaction.commit_on_success
    * @transaction.commit_manually

例如,您可能指示 Django 在视图函数内使用单个事务,并在最后仅当此函数成功返回时提交,如下所示:

from django.http import HttpResponse
from myproj.myapp.models import employees, departments
from django.http import Http404
from django.db import transaction


@transaction.commit_on_success
def newdept(request, emp_id, dept_id, dept_name):
    try:
      new_dept = departments(department_id = dept_id, department_name = dept_name, manager = None)
      new_dept.save()
      emp = employees.objects.get(employee_id__exact = emp_id)
      new_dept.manager = emp
      new_dept.save()
    except employees.DoesNotExist:
      raise Http404
    return HttpResponse("The %s department record has been inserted." %dept_name)


上述 newdept 视图函数仅当成功返回时才自动提交在其内执行的所有操作。如果引发异常,则回滚所有未定更改。但是,如果您删除 newdept 视图函数前面的修饰词或者用 @transaction.autocommit 替代此修饰词,事务行为将发生更改。如果无法找到指定员工,仍将产生 HTTP 404 异常。但此时,第一个 new_dept.save 进行的更改将立即提交到数据库,产生 manager_id 字段为空的部门记录。

需要注意的是,上面列出的代码可以通过 django.shortcuts 模块中定义的 get_object_or_404 函数大大简化。修改如下所示:

...
from django.shortcuts import get_object_or_404


@transaction.commit_on_success
def newdept(request, emp_id, dept_name, dept_id):
    new_dept = departments(department_id = dept_id, department_name = dept_name, manager = None)
    new_dept.save()
    emp = get_object_or_404(employees, employee_id__exact = emp_id)
    new_dept.manager = emp
    new_dept.save()
    return HttpResponse("The %s department record has been inserted." %dept_name )

然而,如果您已选择 commit_manually 修饰程序,则不会选择上述语法。在这种情况下,您需要分别使用 transaction.commit 或 transaction.rollback 显式提交或回滚事务。因此,try-except 语法似乎更适用于这种情况:

...
from django.db import transaction
...
@transaction.commit_manually
def newdept(request, emp_id, dept_id, dept_name):
    try:
      new_dept = departments(department_id = dept_id, department_name = dept_name, manager = None)
      new_dept.save()
      emp = employees.objects.get(employee_id__exact = emp_id)
      new_dept.manager = emp
      new_dept.save()
    except employees.DoesNotExist:
      transaction.rollback()
      raise Http404
    else:
      transaction.commit()
    return HttpResponse("The %s department record has been inserted." %dept_name)

将上述 newdept 视图添加到 myproj/myapp/views.py 中。

创建表示层

如上所述,Django 模板旨在使用 django.template.Context 对象在视图中显示传递给它们的信息。回到在前面的“使用数据库抽象 API”部分中讨论的 index 视图函数,让我们创建在此视图中使用的模板 index.html。

首先,在 myapp 目录内创建一个名为 templates 的目录。默认情况下,这是 Django 查找模板的目录。然后,在 myapp/templates 目录内创建 index.html,并将以下代码插入文件中:

<h1>Managers of departments</h1>
<table border = 1px cellpadding="3" style="font-family:Arial">
<tr>
<th>empid</th>
<th>first name</th>
<th>first name</th>
<th>email</th>
<th>department name</th>
</tr>
{% for department in departments %}
<tr>
<td>{{department.manager.employee_id}}</td>
<td>{{department.manager.first_name}}</td>
<td>{{department.manager.last_name}}</td>
<td>{{department.manager.email}}</td>
<td>{{department.department_name}}</td>
</tr>
{% endfor %}
</table>

模板中的代码非常简单。您使用标准 HTML 标记定义标题和表元素,嵌入 Django 的模板语言元素,这些语言元素的变量标记位于双花括号 {{ ..}} 中,而块标记位于 {% ..%} 对中。要了解有关 Django 模板语言的更多信息,您可以参阅 Django 文档中的“The Django template language”页面。

URL 调度

对于此处讨论的 Django 应用程序,最后一个难题是 URLconf,它应该包含传入请求与视图函数匹配的 URL 模式。虽然您可以完全在项目级别定义这些模式,但将应用程序的 URL 从项目的配置中分离出来被认为是个较好的做法。因此,按如下方式编辑 myproj 目录中的 urls.py 文件:

...
urlpatterns = patterns('',

...

    url(r'^myapp/', include('myproj.myapp.urls')),
)

然后,在 myapp 目录中创建 urls.py 文件,并将以下代码插入其中:

from django.conf.urls.defaults import *
from myapp.views import index, newdept

urlpatterns = patterns('',
url(r'^(?P<emp_id>\d+)/(?P<dept_name>\w+)/(?P<dept_id>\d+)/$' , newdept),
url(r'^$', index),
)

您可能已经猜到,第一种模式旨在处理向“事务管理”部分中讨论的 newdept 视图发出的请求,第二种模式用于向 index 视图发出的请求。

使用 Django 开发 Web 服务器

现在该测试您刚构建的 Django 应用程序了。为此,您可以使用 Django 的内置开发 Web 服务器。首先,从操作系统提示符进入 myproj 目录,并发出以下命令:

manage.py runserver

结果,您应该看到一些告知您开发服务器正在运行的输出行,以及此服务器所在的地址(默认情况下,应该为 http://127.0.0.1:8000/)。剩下的就是将您的浏览器指向 http://127.0.0.1:8000/myapp/。结果应该如下图所示:

要测试 newdept 视图,您可以在 Web 浏览器中输入以下 url:http://localhost:8000/myapp/100/test/1000/。这应该将新记录插入 departments 表中,此记录的 department_id 为 1000,department_name 为 test,manager_id 为 100。

将 Django 用于 Apache

Django 的内置开发 Web 服务器仅适用于测试,这意味着它并不是生产服务器。如果要将它用于生产,您需要慎重考虑。

您可以通过 mod_python 模块(用于在 Apache 内嵌入 Python)将 Django 部署到 Apache。因此,首先确保您已将 mod_python 模块安装到 Apache 服务器上(可以在此处找到详细信息)。然后,您可以将以下 Location 块添加到 Apache 的 httpd.conf 配置文件中(在 PythonPath 中使用实际路径):

< Location "/myapp/">
    SetHandler python-program
    PythonPath "['/home/user/myprojects', '/home/user/myprojects/myproj'] + sys.path"
    PythonHandler django.core.handlers.modpython
    SetEnv DJANGO_SETTINGS_MODULE myproj.settings
    PythonDebug On
< /Location>

重新启动 Apache 之后,您就可以测试应用程序了。如果您的 Apache 服务器在 localhost 上运行,您可以将浏览器指向 http://localhost/myapp/ 以测试 index 视图,指向 http://localhost/myapp/100/test/1000/ 以查看 newdept 是否正常工作。
结论

正如您在本文中了解的那样,Django 是一个功能强大的 Web 框架,可让您迅速创建数据库驱动的 Web 应用程序。您还可以轻而易举地将 Django 连接到 Oracle 数据库。此外,Django 还具有绝佳的 ORM 特性。
分享到:
评论

相关推荐

    原生js图片圆形排列按钮控制3D旋转切换插件.zip

    原生js图片圆形排列按钮控制3D旋转切换插件.zip

    类似c++数组的python包

    内含二维数组与三维数组,分别为list2nd,list3rd

    原生js颜色随机生成9x9乘法表代码.zip

    原生js颜色随机生成9x9乘法表代码.zip

    原生js实现图片叠加滚动切换代码.zip

    原生js实现图片叠加滚动切换代码.zip

    【Academic tailor】学术小裁缝必备知识点:全局注意力机制(GAM)TensorFlow

    【Academic tailor】学术小裁缝必备知识点:全局注意力机制(GAM) 注意力机制是深度学习中的重要技术,尤其在序列到序列(sequence-to-sequence)任务中广泛应用,例如机器翻译、文本摘要和问答系统等。这一机制由 Bahdanau 等人在其论文《Neural Machine Translation by Jointly Learning to Align and Translate》中首次提出。以下将详细介绍这一机制的背景、核心原理及相关公式。 全局注意力机制(Global Attention Mechanism, GAM)由 《Global Attention Mechanism: Retain Information to Enhance Channel-Spatial Interactions》提出,是一篇针对计算机视觉任务提出的方法。这篇文章聚焦于增强深度神经网络中通道和空间维度之间的交互,以提高分类任务的性能。与最早由 Bahdanau 等人提出的用于序列到序列任务的注意力机制 不同,这篇文章的重点是针对图像分类任务,并未专注于序

    基于SpringBoot的“篮球论坛系统”的设计与实现(源码+数据库+文档+PPT).zip

    本项目在开发和设计过程中涉及到原理和技术有: B/S、java技术和MySQL数据库等;此文将按以下章节进行开发设计; 第一章绪论;剖析项目背景,说明研究的内容。 第二章开发技术;系统主要使用了java技术, b/s模式和myspl数据库,并对此做了介绍。 第三章系统分析;包罗了系统总体结构、对系统的性能、功能、流程图进行了分析。 第四章系统设计;对软件功能模块和数据库进行详细设计。 第五章系统总体设计;对系统管理员和用户的功能进行描述, 第六章对系统进行测试, 第七章总结心得;在论文最后结束章节总结了开发这个系统和撰写论文时候自己的总结、感想,包括致谢。

    毕业设计&课设_iOS 商城项目,含购物与商家管理功能,用 Sqlite,有账号示例,适合 iOS 开发练习.zip

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。

    镗夹具总工艺图.dwg

    镗夹具总工艺图

    原生js树叶数字时钟代码.rar

    原生js树叶数字时钟代码.rar

    近代非线性回归分析-韦博成1989

    近代非线性回归分析-韦博成1989

    Rust语言中冒泡排序算法的高效实现与优化

    内容概要:本文详细介绍了用 Rust 语言实现冒泡排序算法的具体步骤,以及通过设置标志位来优化算法性能的方法。示例代码包括了函数定义、内外层循环逻辑、标志位的应用,并在主函数中展示了如何调用 bubble_sort 函数并显示排序前后的数组。 适合人群:具有基本 Rust 编程基础的学习者和开发者。 使用场景及目标:适用于想要深入了解 Rust 中冒泡排序实现方式及其优化技巧的技术人员。通过本篇文章,能够掌握 Rust 基本语法以及算法优化的基本思想。 阅读建议:除了仔细阅读和理解每一部分的内容外,还可以尝试修改代码,改变数据集大小,进一步探索冒泡排序的时间复杂度和优化效果。此外,在实际应用时也可以考虑引入并发或其他高级特性以提升性能。

    培训课件 -安全隐患分类与排查治理.pptx

    培训课件 -安全隐患分类与排查治理.pptx

    1-中国各地级市的海拔标准差-社科数据.zip

    中国各地级市的海拔标准差数据集提供了298个地级市的海拔变异性信息。海拔标准差是衡量某地区海拔高度分布离散程度的统计指标,它通过计算各测量点海拔与平均海拔之间的差异来得出。这一数据对于评估地形起伏对网络基础设施建设的影响尤为重要,因为地形的起伏度不仅会增加建设成本,还会影响信号质量。此外,由于地形起伏度是自然地理变量,它与经济社会因素关联性较小,因此被用作“宽带中国”试点政策的工具变量,以研究网络基础设施建设对经济的影响。数据集中包含了行政区划代码、地区、所属省份、所属地域、长江经济带、经度、纬度以及海拔标准差等关键指标。这些数据来源于地理空间数据云,并以Excel和dta格式提供,方便研究者进行进一步的分析和研究。

    YOLO算法的原理与实现.pdf

    YOLO算法的原理与实现

    机器学习用于视网膜病变预测:使用 XGBoost 揭示年龄和HbA1c 的重要性 -论文

    视网膜病变是糖尿病和高血压的主要微血管并发症。如果不及时治疗,可能会导致失明。据估计,印度三分之一的成年人患有糖尿病或高血压,他们未来患视网膜病变的风险很高。我们研究的目的是检查糖化血红蛋白 (HbA1c)、血压 (BP) 读数和脂质水平与视网膜病变的相关性。我们的主要假设是,血糖控制不佳(表现为高 HbA1c 水平、高血压和异常脂质水平)会导致视网膜病变风险增加。我们使用眼底照相机筛查了 119 名印度患者的视网膜病变,并获取了他们最近的血压、HbA1c 和血脂谱值。然后,我们应用 XGBoost 机器学习算法根据他们的实验室值预测是否存在视网膜病变。我们能够根据这些关键生物标志物高精度地预测视网膜病变。此外,使用 Shapely Additive Explanations (SHAP),我们确定了对模型最重要的两个特征,即年龄和 HbA1c。这表明血糖控制不佳的老年患者更有可能出现视网膜病变。因此,这些高风险人群可以成为早期筛查和干预计划的目标,以防止视网膜病变发展为失明。

    RL Base强化学习:信赖域策略优化(TRPO)算法TensorFlow实现

    在强化学习(RL)领域,如何稳定地优化策略是一个核心挑战。2015 年,由 John Schulman 等人提出的信赖域策略优化(Trust Region Policy Optimization, TRPO)算法为这一问题提供了优雅的解决方案。TRPO 通过限制策略更新的幅度,避免了策略更新过大导致的不稳定问题,是强化学习中经典的策略优化方法之一。

    Spring 应用编译为原生可执行文件.zip

    1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。

    1-中国各地区普通小学毕业生数(1999-2020年)-社科数据.zip

    这组数据涵盖了1999至2020年间中国各地区普通小学毕业生的数量。它为我们提供了一个深入了解中国教育领域中普通小学阶段教育水平和教育资源分配情况的窗口。通过分析这些数据,可以为制定科学合理的教育政策提供依据,同时,通过比较不同城市的普通小学毕业生数,也能为城市规划和劳动力市场调查提供参考。数据来源于中国区域统计年鉴和中国各省市统计年鉴,包含了8472个样本,以面板数据的形式呈现。这些数据对于掌握中国教育态势具有重要的参考价值。

    原生js制作拖拽排列排序代码.zip

    原生js制作拖拽排列排序代码.zip

    PixPin截图工具,非常好用的一款截图工具

    PixPin截图工具,非常好用的一款截图工具

Global site tag (gtag.js) - Google Analytics