[精]Odoo 8.0深入浅出开发教程(五) Odoo开发模块

标签: OdooPython
5407人阅读 评论(0) 收藏 举报
分类:

原文可查看链接:http://blog.sunansheng.com/python/odoo/odoo.html

5 创建自己的模块

Odoo开发的一条黄金准则就是我们最好不要修改现有的模块,特别是官方内置的那些模块。这样做会让原始模块代码和我们的修改混为一谈,使得很难对软件进行升级管理。我们应该创建新的模块(在原有的模块基础之上)来达到修改和扩展功能的目的。Odoo提供了一种继承机制,允许第三方模块扩展现有模块,或者官方的或者来自社区的,这种继承修改可从任意层次来开展,从数据模型到业务逻辑到用户界面。

5.1 快速生成模块骨架

可以如下快速生成一个模块骨架:

./odoo.py scaffold  mymodule myaddons

这将在当前位置新建一个myaddons文件夹,然后在myaddons文件夹下创建一个名字叫mymodule的模块骨架。创建好了之后读者可以进入翻一下。值得一提的是,这个模块骨架并不怎么实用,在学完本章稍微对Odoo的模块结构有所熟悉之后,自己新建文件或者复制一些代码过来可能反而会更加灵活便捷一些。其中有些原因是这个命令生成的模块结构有点老旧了,官方文档的推荐结构如下所示:

addons/<my_module_name>/
|-- __init__.py
|-- __openerp__.py
|-- controllers/
|    |-- __init__.py
|    `-- main.py
|-- data/
|    |-- <main_model>_data.xml
|    `-- <inherited_main_model>_demo.xml
|-- models/
|    |-- __init__.py
|    |-- <main_model>.py
|    `-- <inherited_main_model>.py
|-- security/
|    |-- ir.model.access.csv
|    `-- <main_model>_security.xml
|-- static/
|    |-- img/
|    |-- lib/
|    `-- src/
|        |-- js/
|        |-- css/
|        |-- less/
|        `-- xml/
`-- views/
    |-- <main_model>_templates.xml
    |-- <main_model>_views.xml

这里的文件夹权限推荐是755,文件权限推荐是644。

上面只是一个泛泛而论的情况,具体有些文件夹或文件可能是不需要的。下面对这些内容进一步说明之。

5.1.1 python模块的init文件

Odoo模板本质上就是一个python模块,所以首先它应该有一个 __init__.py 文件。刚开始里面不放任何内容都是可以的。

而scaffold自动创建有下面的内容:

import controllers
import models

推荐改成这样的形式(有更好的python2和python3兼容性):

from .models import main_model

这种相对路径语法python2和python3都是通用的,具体对应的就是本目录下的models目录下的main_model.py文件。推荐将所有的模型定义python文件都放入models文件夹中,models文件夹下也应该有一个 __init__.py 文件。然后这里还新建了一个main_model.py文件,这个文件的名字倒是随意的了。

5.1.2 作为Odoo模块的说明文件

然后本模块作为Odoo框架的模块还必须新建一个 __openerp__.py 文件,最小型的什么都不做的Odoo模块就需要这两个文件,一个是这个 __openerp__.py 文件,一个是 __init__.py 文件。

scaffold自动创建的 __openerp__.py 文件大致内容如下:

# -*- coding: utf-8 -*-
{
    'name': "mymodule",

    'summary': """
        我的第一个模块
        """,

    'description': """
        我的第一个模块,用于学习自定义模块。
    """,

    'author': "wanze",
    'website': "http://www.yourcompany.com",

    # Categories can be used to filter modules in modules listing
    # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
    # for the full list
    'category': 'Test',
    'version': '0.1',

    # any module necessary for this one to work correctly
    'depends': ['website'],

    # always loaded
    'data': [
        'security/ir.model.access.csv',
        'views/mymodule_templates.xml',

        'demo.xml',
    ],
    # only loaded in demonstration mode
    'demo': [
        'demo.xml',
    ],
}

以后我们要创建一个新的模块,这个文件的格式可以复制粘贴过去。其中一些基本的比如name就是本模块的名字,然后description还有category分类等等就不多说了,这些含义都是很明显的,就是本模块的一些描述信息。然后提得一提的是这三个属性:

depends
本模块的依赖关系,这里主要是指本模块对于Odoo框架内其他的模块的依赖。如果本模块实在没什么依赖,就把 base 模块填上去。
data
本模块要加载的数据文件,别看是数据文件,似乎不怎么重要,其实Odoo里面视图,动作,工作流,模型具体对象等等几乎大部分内容都是通过数据文件定义的。具体这些xml或csv文件如何放置后面再讲。
demo
这里定义的数据文件正常情况下不会加载,只有在demonstration模式下才会加载,具体就是你新建某个数据库是勾选上了加载演示数据那个选项。如下图所示:

加载演示数据.png

Figure 14: 加载演示数据

这可能并不是你想要的效果,因为其他官方内置模块也附加很多演示信息进来了。其实读者一定也想到了,我们完全可以将demo.xml放入 "data" 那里,然后实际运作的时候不加载就是了,我更喜欢这种处理方案。

5.1.2.1 属性值清单:
name
模块名字
summary
可以看作简短介绍吧
description
可以看作详细介绍
author
模块作者
website
模块网站
category
模块分类
version
模块版本号
license
模块版权信息,默认是AGPL-3
depends
模块依赖
data
模块必加载的数据文件
demo
demonstration下才加载的数据文件
installabel
默认True,可设为False禁用该模块
auto_install
默认False,如果设为True,则根据其依赖模块,如果依赖模块都安装了,那么这个模块将自动安装,这种模块通常作为胶合(glue)模块。
application
默认False,如果设为True,则这个模块成为一个应用了。你的主要模块建议设置为True,这样进入Odoo后点击本地模块,然后默认的搜索过滤就是 应用 ,这样你的主模块会显示出来。

5.2 安装自定义模块

就设置好这样两个文件,虽然里面什么内容都没有,实际上也就可以开始安装这个模块而来。

前面说了设置 --addons-path=addons, myaddons ,就可以加载自定义的模块了。具体安装就和安装其他模块没有两样,除了你需要清除搜索栏然后输入搜索关键词。然后注意如果不是新建的数据库,那么需要打开技术特性执行, 更新模块列表 之后,才能搜索到你自己定义的新的模块。

自定义模块如果修改之后,(不需要重新编译Odoo), 肯定是需要重启Odoo的 ,然后进去之后如果你的模块增减了额外的文件,则还需要升级(update)相应的模块。

5.2.1 模块文件夹管理

  • data文件夹 ,放着demo和data xml
  • models文件夹,放着模型定义
  • controllers文件夹,http路径控制
  • views文件夹,网页视图和模板
  • static文件夹,网页的一些资源,里面还有子文件夹:css,js,img,lib等等。

5.3 一个简单的演示模块

5.3.1 controllers

现在我们在controllers文件夹里新建一个 __init__.py ,然后新建一个main.py文件。在main.py文件中添加如下内容:

class Mymodule(http.Controller):@http.route('/mymodule/mymodule/', auth='public')def index(self, **kw):return "Hello, world"

现在请读者如下所示安装和更新自定义模块之后,进入 127.0.0.1:8089/mymoduel/mymodule 来看一下效果。这样我们就明白了这里的 @http.route 装饰器有根据具体某个函数的返回信息进行路径分发和控制访问权限的功能。

如果没有什么问题,你应该能看到一行hello world文字,祝你好运。

5.3.2 views

views文件夹用于视图控制,里面通常放着一些模板文件等。我们首先修改 __openerp__.py 文件中data属性为这个样子。

'data': [
    'views/mymodule_templates.xml',
],

然后按照这个在views文件夹里面创建一个mymodule_templates.xml文件。

<openerp><data><template id="index"><title>MyModule</title><t t-foreach="fruits" t-as="fruit"><p><t t-esc="fruit"/></p></t></template></data></openerp>

这里使用了Qweb模板语言,就这里提及的我们可以简单了解下:

<t t-foreach="[1, 2, 3]" t-as="i">
<p><t t-esc="i"/></p>
</t>

其输出是:

<p>1</p>
<p>2</p>
<p>3</p>

如果对应python语言的话,可以理解为:

for i in [1, 2, 3]:print('<p>{i}</p>'.format(i = i))

这里的 <t t-esc="i"/> 是先计算 i 的值,然后将其打印出来。

然后在之前controllers那里的main.py文件那里,我们使用这个模板文件。

from openerp import httpclass Mymodule(http.Controller):@http.route('/mymodule/mymodule/', auth='public')def index(self, **kw):return http.request.render("mymodule.index",{'fruits':['apple','banana','pear']})

如果不出意外的话,你应该看到这样的画面了:

mymodule_index.png

这里调用http.request.render函数,可以猜到这是一个网页模板渲染输出函数。然后注意看第一个参数, mymodule.index ,这里还是有一些讲究的,mymodule就是你正在编写的这个模块的名字,然后这个index对应的就是那个网页模板文件的id属性: <template id="index"> 。接下来的第二个参数就是给模板传递进去的一些变量值了。

5.3.3 models

一般一些数据对象或类就定义在models.py文件里,如果类继承自 models.Model 类,那么这个类就有了Odoo字节写的ORM接口了。也就是你定义的这些模型生成的对象都会存入SQL数据库中。

我们在models文件夹里面新加入一个 __init__.py 文件,然后再新建一个 main_module.py 文件, 在其内写上一个简单的模型定义,具体如下所示:

class Fruits(models.Model):_name = 'mymodule.fruits'name = fields.Char()

然后 __init__.py 那边也注意加载好模块,这里就不多说了。

新加入一个演示数据 demo.xml :

<openerp><data><record id="apple" model="mymodule.fruits"><field name="name">apple</field></record><record id="banana" model="mymodule.fruits"><field name="name">banana</field></record><record id="pear" model="mymodule.fruits"><field name="name">pear</field></record></data></openerp>

在 __openerp__.py 的demo那里设置成这样:

'data': [
    'security/ir.model.access.csv',
    'views/mymodule_templates.xml',

    'demo.xml',
],

然后 controllers/main.py 改成这样了:

from openerp import httpclass Mymodule(http.Controller):@http.route('/mymodule/mymodule/', auth='public')def index(self, **kw):fruits = http.request.env['mymodule.fruits']return http.request.render("mymodule.index",{'fruits': fruits.search([])})

我们知道 demo.xml 里面定义的数据都放入全局环境里面去了。这里具体的细节还需要进一步讨论,不过我们可以猜测通过 http.request.env 可以来引用这个全局环境的字典值,来查找 'mymodule.fruits' 的值,然后对这个值进行了 search 操作,这里具体的细节还需要进一步讨论,但我们可以猜得其最后返回的是一个列表值,然后里面存储的数据都是Fruit模型建立的对象。

而 views/mymodule_templates.xml 改成这样子了:

<openerp><data><template id="index"><title>MyModule</title><t t-foreach="fruits" t-as="fruit"><p><t t-esc="fruit.id"/><t t-esc="fruit.name"></t></p></t></template></data></openerp>

这里引用了fruit的id属性和name属性。我们可以看到这样的输出结果:

mymodule_index2.png

刚开始id是从1,2,3开始的,因为我运行过几次,在这里成7,8,9了。

哦,最后我们还需要修改一个东西:

5.3.4 security

security文件夹里面已经有一个 ir.model.access.csv 文件了,其对模型的访问权限进行管理,大致修改内容如下:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mymodule_fruits,mymodule.fruits,model_mymodule_fruits,,1,0,0,0

目前还不太懂,看到perm_read那一列,要设置为1,这样所有用户可读。然后一些名字是根据你定义的模型的名字变化的。

5.3.5 美化网页

Odoo框架里面已经内置了一个帮助你设计网页的website builder模块,然后这里我们可以通过调用这个模块的结构来简单看一下。

首先是 __openerp__.py 的"depends" 属性修改一下:

'depends': ['website'],

然后是controllers的main.py那里加上 website=True 这个设置。

@http.route('/mymodule/mymodule/', auth='public', website=True)

最后是模块文件那里修改为:

<openerp>
<data>
    <template id="index">
    <t t-call="website.layout">
    <t t-set="title">MyModule</t>
    <div class="oe_structure">
        <div class="container">
        <t t-foreach="fruits" t-as="fruit">
        <p><t t-esc="fruit.id"/><t t-esc="fruit.name"></t></p>
        </t>
        </div>
    </div>
    </t>
</template>
</data>
</openerp>

请读者自己试着运行一下,看看效果,然后里面还有很多其他的设置功能等等。这里的细节先略过了。

5.4 加分项:通过pgadmin3来查看数据库

pgadmin3是针对PostgreSQL数据库很有名的一个管理员工具,里面的经很多,这里只是简单谈论一下。

5.4.1 安装

在ubuntu下可以用apt-get简单安装之。

sudo apt-get install pgadmin3

5.4.2 连接服务器

刚进入软件需要连接服务器,如下图所示:

pgadmin3连接服务器.png

Figure 17: pgadmin3连接服务器

目前我已经确认的就是名称随意填,然后主机不能填127.0.0.1,而只能填"localhost"这个字符串。然后后面的应该不用更改什么了,如果你给你的postgres数据库设置密码了,那么这个密码也需要填上。

连接好服务器了,我们就可以双击或点击查看你的PostgreSQL数据库的信息了。

5.4.3 图形化查询

在工具那里有很多有用的工具,比如查看服务器状态工具等。然后我们点击查询工具,会弹出一个窗口,看到图形化查询那个子选单,如下图所示:

pgadmin3图形化查询.png

Figure 18: pgadmin3图形化查询

这里我找到了前面我们新建的那个fruits模型,可以看到所有的fruits模型是放在一个名字叫做mymodule_fruits 的SQL表格里的。然后它的表头有:id, create_uid, create_date, name等。

然后我们切换到SQL编辑器子选单。这个工具会根据你的图形化查询选择结果自动生成对应的SQL查询语句:

SELECT 
  * 
FROM 
  public.mymodule_fruits;

所以在我们这个odoo数据库里面,我们新建的模型fruits的各个对象,具体存入的table名是public.mymodule_fruits 。

然后我们点击 查询→执行 ,来具体执行这个SQL语句,输出结果如下所示:

pgadmin3具体查询结果.png

6 Odoo开发基础: 请假模块第一谈

6.1 纯理论讨论

在实际编写前先谈谈理论,这部分理论讨论非常有用,对于具体编写模块的时候你清楚自己在感谢什么很有帮助。感谢老肖的《OpenERP 应用和开发基础》一书,该书第六章对我帮助很大。

首先我们需要一个菜单,那么这个菜单在Odoo框架中是如何生成的呢?前面谈到Odoo的模型具体的对象实际上就是SQL表格的一条记录,而Odoo框架具体显示的菜单也是一个Odoo中的一个对象,其对应的表格是 ir_ui_menu ,其在xml中的声明是通过menuitem标签来完成的,具体细节等下再讲。然后菜单需要连接一个动作,这样用户点击这个菜单的时候,这个动作将会触发。

这些动作对象(和窗口操作相关的)是存放在 ir_act_window 表格中的。动作触发之后接下来是要处理视图问题,首先根据 ir_act_window_view 表格来找到具体关联的某个视图对象,具体某个视图对象是存放在 ir_ui_view 表格中的。然后根据具体关联的某个模型的某个对象的具体的值来构建出显示画面。

具体研究对象的模型,视图,菜单,动作等,这些实际上都是Odoo里面的模型,也就是具体对象的值是存放在某个具体的SQL表格里的,然后程序完成一系列的索引,取值等操作,并最终生成显示结果,这大概就是Odoo框架里面发生的故事概貌了。

按照上面的讨论,等下我们的工作有:

  1. 具体研究对象的模型,这里是请假单模型,然后请假单模型里面应该有的field有: 申请人,请假天数,开始休假日期,请假事由。
  2. 构建菜单对象。
  3. 构建动作对象,并与具体的某个菜单关联起来。
  4. 构建视图对象。

__init__.py 文件内容如下:

# -*- coding: utf-8 -*-from .models import main_model

__openerp__.py 文件内容如下:

# -*- coding: utf-8 -*-
{
    'name': "qingjia",

    'summary': """
        请假模块,提供请假功能
        """,

    'description': """
        请假模块,提供请假功能。
    """,

    'author': "wanze",
    'website': "http://www.yourcompany.com",

    # Categories can be used to filter modules in modules listing
    # Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
    # for the full list
    'category': 'Test',
    'version': '0.1',

    # any module necessary for this one to work correctly
    'depends': ['base'],

    # always loaded
    'data': [
        'security/ir.model.access.csv',
        'views/views.xml',
    ],
    # only loaded in demonstration mode
    'demo': [
        'demo.xml',
    ],
    'application' : True,
}

首先我们来看下main_model.py文件里面定义的模型是怎样的:

6.2 定义模型

from openerp import models, fields, apiclass Qingjd(models.Model):_name = 'qingjia.qingjd'name = fields.Many2one('res.users', string="申请人", required=True)days = fields.Float(string="天数", required=True)startdate = fields.Date(string="开始日期", required=True)reason = fields.Text(string="请假事由")def send_qingjd(self):self.sended = Truereturn self.sendeddef confirm_qingjd(self):self.state = 'confirmed'return self.state

这种模型定义语法结构我们大体是熟悉的了,下面定义的两个方法等下会用到的,等下再谈。然后Float是浮点类型,Date是日期类型这个是一目了然的,然后Text是大段文本对象,string是这个field的用户界面显示的文字,required设置为True则该值为必填项。然后这个 Many2one 还有 res.users是什么?

首先让我们看看 public.res.users 这个表格的值:

res.users表格.png

Figure 20: res.users表格

这里Many2one的意思像是我从很多(Many)相同模型的对象中取一个(one)的意思。等下我们会看到一个下拉选单。更多细节需要深入学习Odoo ORM的API细节。

6.3 加入菜单

接下来的工作就是在views/views.xml文件里面定义具体的菜单对象。

代码第一版如下所示:

<?xml version="1.0"?>
<openerp>
<data>
    <record id="action_qingjia_qingjd" model="ir.actions.act_window">
        <field name="name">请假单</field>
        <field name="res_model">qingjia.qingjd</field>
        <field name="view_mode">tree,form</field>
    </record>

    <menuitem id="menu_qingjia" name="请假" sequence="0"></menuitem>
    <menuitem id="menu_qingjia_qingjiadan" name="请假单" parent="menu_qingjia"></menuitem>
    <menuitem id="menu_qingjia_qingjiadan_qingjiadan" parent="menu_qingjia_qingjiadan" action="action_qingjia_qingjd"></menuitem>

</data>
</openerp>

这种record语法我们已经有所熟悉了,然后model是 ir.actions.act_window ,我们可以在源码openerp→addons→base→ir中找到 ir_actions.py 文件,然后有下面的代码:

class ir_actions_act_window(osv.osv):
    _name = 'ir.actions.act_window'
    _table = 'ir_act_window'

我们可以看到其对应的正是表格 ir_act_window 。

field是用来填充具体表格的某个列值的,我们还可以使用如下简便的语法:

<act_window id="action_qingjia_qingjd"
    name="请假单"
    res_model="qingjia.qingjd"
    view_mode="tree,form" />

这里的标签 act_widow 还不清楚是在那里规定的,然后下面具体的菜单对象也简便使用了menuitem 标签。

我们可以在openerp→addons→base→ir中找到 ir_ui_menu.py 文件,有如下代码:

class ir_ui_menu(osv.osv):
    _name = 'ir.ui.menu'

可以看到菜单对象对应的是 ir_ui_menu 表格——和数据模型一样的映射法则,_name的点号变成下划线。至于菜单对象为何对应 menuitem 标签还不清楚那里固定的。

这两个对象具体的一些属性说明一下:

6.3.1 act_window 的属性

name
具体act_window动作在UI中显示的名字(类似于QT中动作作为菜单中的项目的情况)。
res_model
act_window动作对应的某个数据模型(这里动作和数据模型关联在一起了)
view_mode
act_window动作打开后支持的视图模式。

6.3.2 menuitem 的属性

name
具体这个菜单在视图中显示的名字。
sequence
是显示排序(还不太懂)。
parent
是本菜单的父菜单。如果是子菜单则需要指定,只有顶级菜单不需要指定。
action
指定本菜单连接的动作。如果连接动作了那么name属性可以不用指定了,系统会直接引用动作的name属性的。这里菜单和某个动作关联起来了。和前面联系起来,那么就是具体某个子菜单和某个数据模型关联起来了。

这样现在你应该看到类似如下的视图了:

请假单_tree01.png

请假单_form01.png

6.4 视图优化

最主要的三个视图是list或者tree视图;表单form视图和search搜索视图。视图都属于ir.ui.view模型

6.4.1 修改tree视图

下面定义了一个自己的tree视图:

<record id="qingjia_qingjd_tree" model="ir.ui.view">
<field name="name">qing jia dan tree</field>
<field name="model">qingjia.qingjd</field>
<field name="arch" type="xml">
    <tree>
        <field name="name"/>
        <field name="startdate"/>
        <field name="days"/>
    </tree>
</field>
</record>

这里的name属性是本视图的名字,似乎没什么意义。然后 model 属性很重要,前面子菜单关联到了某个动作,然后某个动作关联到了某个数据模型了,这里就是将这个视图和某个模型关联起来了。

下面的 arch 这个格式还不清楚有什么用,但必须这么写:

<field name="arch" type="xml">
    ...
</field>

然后接下来定义tree视图,又叫列表视图。其中的field就是对应的具体那个数据模型的field,加入谁就要显示谁。

6.4.2 修改form视图

下面定义一个自己的form视图:

<record id="qingjia_qingjd_form" model="ir.ui.view">
<field name="name">qing jia dan form</field>
<field name="model">qingjia.qingjd</field>
<field name="arch" type="xml">
    <form>
    <sheet>
    <label for="name"/> <field name="name"/>
    <label for="days"/><field name="days"/>
    <label for="startdate"/><field name="startdate"/>
    <label for="reason"/><field name="reason"/>
    </sheet>
    </form>
</field>
</record>

其他都类似上面的,不赘述了。

重点在form标签里面。这里引入了sheet布局,然后label加入标签 field引入具体属性。

然后我们还可以使用 header 标签引入表单视图的头部分。头部分里面一般放着一些按钮动作。比如:

<header>
    <button name="send_qingjd" type="object"
    string="发送" class="oe_highlight" />
    <button name="confirm_qingjd" type="object"
    string="确认" />
</header>

这里string是这个按钮具体显示的字符,然后name是这个按钮具体应该执行的动作(对应本模型的该名字的方法),class="oe_highlight" 让按钮变为红色突出显示。

6.4.2.1 使用group布局

此外还可以使用group布局,group布局还不太懂,其中可以引入 string 属性,等下可以作为group的标题显示出来。

这是我的这个简单的请假单布局

<sheet>
    <group name="group_top" string="请假单">
        <field name="name"/>
        <field name="days"/>
        <field name="startdate"/>
        <field name="reason"/>
    </group>
</sheet>

6.5 完整的views.xml

至此完整的views.xml文件如下所示:

<?xml version="1.0"?><openerp><data><!--    打开请假单动作--><act_window id="action_qingjia_qingjd"name="请假单"res_model="qingjia.qingjd"view_mode="tree,form" /><!--表单视图--><record id="qingjia_qingjd_form" model="ir.ui.view"><field name="name">qing jia dan form</field><field name="model">qingjia.qingjd</field><field name="arch" type="xml"><form><header><button name="send_qingjd" type="object"string="发送" class="oe_highlight" /><button name="confirm_qingjd" type="object"string="确认" /></header><sheet><group name="group_top" string="请假单"><field name="name"/><field name="days"/><field name="startdate"/><field name="reason"/></group></sheet></form></field></record><!--tree视图--><record id="qingjia_qingjd_tree" model="ir.ui.view"><field name="name">qing jia dan tree</field><field name="model">qingjia.qingjd</field><field name="arch" type="xml"><tree><field name="name"/><field name="startdate"/><field name="days"/></tree></field></record><!--加入菜单--><menuitem id="menu_qingjia" name="请假" sequence="0"></menuitem><menuitem id="menu_qingjia_qingjiadan" name="请假单" parent="menu_qingjia"></menuitem><menuitem id="menu_qingjia_qingjiadan_qingjiadan" parent="menu_qingjia_qingjiadan" action="action_qingjia_qingjd"></menuitem></data></openerp>

至此显示效果如下所示:

请假单_tree02.png

请假单_form02.png

6.6 给模块加个图标

模块 static→description 的 icon.png 文件就对应模块的图标。把它设置好你的模块就有一个图标了。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1654990次
    • 积分:14084
    • 等级:
    • 排名:第937名
    • 原创:192篇
    • 转载:13篇
    • 译文:0篇
    • 评论:43条
    技术交流

    苏南生的博客
    主页 | BOOK搜索 | 免费杂志 |
    博客专栏
    最新评论