odoo12从零开始:三、1)创建你的第一个应用模型(module)
前言
以前,我一直都不知道为什么好多框架的入门都是“hello world”开始,当我思前想后我要如何介绍odoo的model、record、template等继承等高级特性时,发现在那之前便需要清楚地介绍什么是模型(model),什么是记录(record),什么是模板(template),以及他们到底是干什么用以及是怎么用的?想要知道它们是怎么用的,就得介绍odoo的一个应用模块(module)的结构是什么样的。那么了解一个应用结构最快的办法,那就是我们自己去完成一个。Let's do it!
Tips: 初学者容易搞混模块(module)和模型(model)。
模块(module):是一个odoo应用,包含模型(models)、控制器(controllers)、视图(views)、权限(ir.rule,ir.group, ir.model.access)、初始化数据(data)、报表(report)、向导(wizard)、静态文件(static)等。
模型(model):是模块(module)的一部分,是odoo的ORM的描述对象,它的工作是帮我们将内存中的对象反映为数据库中的关系数据。
模块结构(Module Structure)
主要目录:
data/ : demo和数据的xml文件
models/ : models定义
controllers/ : 包含controllers (HTTP路由等)
views/ : 包含视图(views)和模板(templates)
static/ : 包含web资源, 分为css/, js/, img/, lib/等
其他可选目录结构:
wizard/ : 向导,由瞬时模型(models.TransientModel)构成,以及向导的视图(views)
report/ : 报表,包含含有sql的模型,XML文件等
tests/ : 测试代码
项目结构示意图
addons/模块名/
|-- __init__.py
|-- __manifest__.py (描述文件)
|-- controllers/
| |-- __init__.py
| |-- main.py
| |-- *****.py
|-- data/
| |-- *****_data.xml
| |-- *****_demo.xml
|-- models/
| |-- __init__.py
| |-- *****.py
|-- report/
| |-- __init__.py
| |-- *****_report.py
| |-- *****_report_views.xml
| |-- *****_reports.xml (report actions, paperformat, ...)
| |-- *****_templates.xml (xml report templates)
|-- security/
| |-- ir.model.access.csv
| |-- *****_groups.xml
| |-- *****_security.xml
|-- static/
| |-- description/
| | |-- icon.png(模块的icon)
| |-- img/
| | |-- *****.png
| | |-- *****.jpg
| |-- lib/
| | |-- external_lib/
| |-- src/
| | |-- js/
| | | |-- widget_a.js
| | | |-- widget_b.js
| | |-- scss/
| | | |-- widget_a.scss
| | | |-- widget_b.scss
| | |-- xml/
| | | |-- widget_a.xml
| | | |-- widget_a.xml
|-- views/
| |-- assets.xml
| |-- **********.xml
|-- wizard/
| |--*****.py
| |--*****.xml
本节代码
git clone -b v3.1 https://github.com/lingjiawen/odoo_project.git
开发模块(module)
我们基于上一节的代码版本(V2.1)中进行开发:
git clone -b v2.1 https://github.com/lingjiawen/odoo_project.git
我们先来尝试仿照官方的hr模块开发一个简易版的员工模块,包含员工基本信息,员工部门和员工职位管理。
1、创建用户目录
首先,我们在my_addons/下新建employee/目录,在目录下新建以下文件:
my_addons/employee/
|-- __init__.py
|-- __manifest__.py
|-- models/
| |-- __init__.py
|-- views/
2、填写描述文件__manifest__.py
# -*- coding: utf-8 -*-
{
'name': 'Employee',
'version': '12.0.1.0',
'summary': '对员工的基本信息,部门和职位进行管理',
'description': '''
员工管理模块
''',
'author': 'misterling',
'sequence': 15,
'category': 'Uncategorized',
'license': 'LGPL-3',
'depends': ['base'],
'data': [],
'demo': [],
'qweb': [],
'installable': True,
'application': True,
'auto_install': False,
# 'pre_init_hook': '',
# 'post_init_hook': '',
# 'uninstall_hook': '',
}
以上描述便是常用的配置项,我们来看看它们都代表着什么:
name: 模块的标题
version: 版本号
summary: 模块的子标题
description: 模块的描述性文字
author: 作者
sequence: 模块在apps中的排列的序号,影响展示顺序。
category: 模块的分类,在设置->用户&公司->群组中"应用"字段中可以看到
license: 代表着你的开源协议。
depends: 依赖的模块,在安装当前模块时,如果依赖模块未安装,将会自动安装;升级依赖的模块时,所有依赖它的模块也将会跟着升级。
data: 加载XML文件。
demo: 加载demo文件。
qweb: 加载qweb template文件。
installable: 是否可以安装。
application: 是否是应用,在应用列表中,被应用筛选隔离,好的开发习惯应该谨慎考虑是否是应用。
auto_install: 是否自动安装,设为True的应用将在数据库初始化时自动安装
pre_init_hook: 顾名思义,模块安装前的钩子,指定方法名即可
post_init_hook: 模块安装完成后的钩子
uninstall_hook: 模块卸载时的钩子
Tips:我们在书写python文件时,不用忘记在首部添加
# -*- coding: utf-8 -*-
以支持中文编码
3、创建员工对象
我们在models/下面新建employee.py文件,编写以下内容:
# -*- coding: utf-8 -*-
import base64
import logging from odoo import api, fields, models
from odoo.modules.module import get_module_resource
from odoo import tools, _ _logger = logging.getLogger(__name__) GENDER = [
('male', u'男'),
('female', u'女'),
('other', u'其他')
] MARITAL = [
('single', u'单身'),
('married', '已婚'),
('cohabitant', '合法同居'),
('widower', '丧偶'),
('divorced', '离婚')
] class Employee(models.Model):
_name = "ml.employee"
_description = '''
员工信息
''' @api.model
def _default_image(self):
image_path = get_module_resource('hr', 'static/src/img', 'default_image.png')
return tools.image_resize_image_big(base64.b64encode(open(image_path, 'rb').read())) name = fields.Char(string=u'姓名') # image
image = fields.Binary(string=u"照片", default=_default_image, attachment=True, help=u"上传员工照片,<1024x1024px")
image_medium = fields.Binary(string=u"中尺寸照片", attachment=True, help="128x128px照片")
image_small = fields.Binary(string=u"小尺寸照片", attachment=True, help="64x64px照片") company_id = fields.Many2one('res.company', string=u'公司') gender = fields.Selection(GENDER, string=u'性别')
country_id = fields.Many2one('res.country', string=u'国籍')
birthday = fields.Date(string=u'生日')
marital = fields.Selection(MARITAL, string=u'婚姻状况', default='single') # work
address = fields.Char(string=u'家庭住址')
mobile_phone = fields.Char(string=u'手机号码')
work_email = fields.Char(string=u'工作邮箱')
leader_id = fields.Many2one('ml.employee', string=u'所属上级')
subordinate_ids = fields.One2many('ml.employee', 'leader_id', string=u'下属') note = fields.Text(string=u'备注信息') @api.model
def create(self, values):
tools.image_resize_images(values)
return super(Employee, self).create(values) @api.multi
def write(self, values):
tools.image_resize_images(values)
return super(Employee, self).write(values)
我们一起来梳理一下类文件的主要内容:
from odoo import api, fields, models 引入 api, fields, models 1、class类
odoo的class继承了models.Model类,是odoo最常用的模型类,其他的还有
models.TransientModel,瞬时模型,用于向导(wizard),系统会在一定时间后自动清除模型的记录
models.AbstractModel,抽象模型,和抽象类是一样的概念,系统不会为该模型建立数据库表 2、内部标识
_name = "ml.employee",为odoo类的唯一标识,如果没有指定_table属性,那么系统将会为该模型建立数据库表名为ml_employee的数据表。
_description:主要为描述信息 3、使用的字段
odoo模型的字段使用fields.xxx来声明。
1)Char:文本字段
2)Binary:二进制字段,通常用于图片、附件等文件读写
3)Many2one:多对一关系字段,如:
company_id = fields.Many2one('res.company', string=u'公司')
表现为多个员工可以对应同一个公司,'res.company'是odoo内置公司模型
4)Selection:列表选择字段,第一个参数为元组列表,表示可选列表
5)Date: 日期控件字段
6)One2many:一对多关系字段,如:
subordinate_ids = fields.One2many('ml.employee', 'leader_id', string=u'下属')
表示一个员工可以有多个下属
7)Text: 文本字段,在前端表现为textarea,char在前端表现为input 4、属性
string:表示字段的显示名称
default:表示字段的默认值
attachment:binary字段的特有属性,表现为是否以附件的形式存储,设为True时,将会存储到ir.attachment中 5、ORM 方法修饰器
@api.model:模型修饰器,相当于静态方法,方法将为模型类共有,而不是每个实例。
@api.multi:对记录集执行一些操作,方法的逻辑通常会包含对 self 的遍历 6、方法
1)_default_image:我们获取hr模块目录下的图片,赋值给image字段作为默认值
2)create、write:重写记录的创建、编辑方法,使用odoo自带的工具image_resize_images对image_medium,image_small进行赋值
写完employee类之后,我们在与其同级的__init__.py中引入:
# -*- coding: utf-8 -*- from . import employee
再在与models/目录同级的__init__.py中引入models:
# -*- coding: utf-8 -*- from . import models
到此,我们就已经创建好了employee的类模型,接着我们要为其写视图(views)
4、编写视图
我们先来看看odoo最常用的三种视图:树形(tree),表单(form),搜索(search),它们存储于odoo内置的ir.ui.view模型中,其他还有图形(graph)、透视表(pivot)、日历(calendar)、图标(diagram)、甘特图(gantt)、看板(kanban)、QWEB、活动(activity),是odoo的最主要的页面展现形式。
1)tree视图
tree视图为模型记录(record)的列表展示形式
2)form视图
form视图为表单展现形式,主要用于odoo记录的创建,编辑。
3)search视图
search视图主要用于在tree、kanban等视图中进行搜索、过滤、分组记录以方便查看。
我们为我们的员工模型书写这三种视图:
新建views/employee.xml文件,加入odoo data标签:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
</data>
</odoo>
先在data内增加一个form视图:
<record id="view_ml_employee_form" model="ir.ui.view">
<field name="name">员工信息表单</field>
<field name="model">ml.employee</field>
<field name="arch" type="xml">
<form string="员工信息">
<sheet>
<field name="image" widget='image' class="oe_avatar"
options='{"preview_image":"image_medium"}'/>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="员工姓名" required="True"/>
</h1>
</div>
<notebook>
<page string="员工信息">
<group>
<group string="基本信息">
<field name="gender" required="True"/>
<field name="country_id"/>
<field name="birthday"/>
<field name="marital"/>
</group>
<group string="工作信息">
<field name="company_id" options="{'no_open': True, 'no_create': True}" groups="base.group_multi_company"/>
<field name="address"/>
<field name="mobile_phone" widget="phone"/>
<field name="work_email" widget="email"/>
<field name="leader_id" options="{'no_open': True, 'no_create': True}"/>
</group>
</group>
</page>
<page string="下属信息">
<field name="subordinate_ids">
<tree editable="bottom">
<field name="name" attrs="{'required': True}"/>
<field name="gender" required="True"/>
<field name="country_id"/>
<field name="mobile_phone"/>
<field name="work_email"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
我们来看看form表单的写法:
写一个form表单,实质上在为模型ir.ui.view增加一条记录,odoo中为模型增加一条记录可以使用record标签,我们为它取了唯一的id:view_ml_employee_form(我们约定,记录的书写使用view_模型名_form/tree的命名方式),然后使用record内的model属性指定增加的记录属于ir.ui.view模型。
我们先使用field标签插入name和model,
<field name="name">员工信息表单</field>
<field name="model">ml.employee</field>
代表我们是为ml.employee模型书写的form视图
我们在系统设置中打开“开发者模式”,然后打开设置->技术->用户界面->视图,可以看到系统中现在已有的记录(完成开发并升级后):
我们使用
<field name="arch" type="xml">
</field>
插入form内容。
其中:
1、使用<form></form>标签包裹表示记录类型为form视图
2、使用<field name="属性名" />的方式显示字段
3、<notebook>
<page>
</page>
<page>
</page>
……
</notebook>
为翻页标签
4、widget='image'为显示类型为图片
5、required="True"为必填,常用的还有invisible、readonly等
6、需要使用group包裹field以正常显示字段的string值
One2many字段有特定的写法:
<field name="subordinate_ids">
<tree editable="bottom">
<field name="name" attrs="{'required': True}"/>
<field name="gender" required="True"/>
<field name="country_id"/>
<field name="mobile_phone"/>
<field name="work_email"/>
</tree>
</field>
字段内嵌tree视图来自定义显示方式,editable="bottom"表示不弹出新窗口来创建明细记录。
细心的朋友可能看到必填有 required=True 和 attrs="{'required': True}"两种写法,事实上,invisible, readonly也有这两种写法。
他们的区别在于:
required=True:这个写法是死的,在视图加载时就已经确定。
attrs="{"required": [('name', '!=', False)]}: 这种写法可以书写domain来过滤(上面的写法也可以)。最重要的是,它会随着name的变化来动态改变required的值。
更多详细的介绍我们将会在后面views的专章介绍,这里只要了解大概就可以了。
options="{'no_open': True, 'no_create': True}"
其主要作用在对于Many2one字段,不允许其打开和新建它的专有视图。
Tips:
我们约定:many2one字段的命名使用xxx_id,如leader_id;
One2many字段的命名我们使用xxx_ids,如subordinate_ids;
我们再为它书写tree视图:
<record id="view_ml_employee_tree" model="ir.ui.view">
<field name="name">员工信息列表</field>
<field name="model">ml.employee</field>
<field name="arch" type="xml">
<tree string="员工信息">
<field name="name"/>
<field name="company_id"/>
<field name="gender"/>
<field name="country_id"/>
<field name="mobile_phone"/>
<field name="work_email"/>
<field name="leader_id"/>
</tree>
</field>
</record>
Tree视图相对比较简单:
1、<tree></tree>包裹表示为tree视图
2、罗列字段以确定列表的显示字段以及显示顺序
我们再为其添加Search视图:
<record id="view_ml_employee_filter" model="ir.ui.view">
<field name="name">员工搜索视图</field>
<field name="model">ml.employee</field>
<field name="arch" type="xml">
<search string="员工">
<!--用于搜索的字段-->
<field name="name" string="员工"
filter_domain="['|',('work_email','ilike',self),('name','ilike',self)]"/>
<field name="gender" string="性别"/>
<separator/>
<!--定义好的过滤器-->
<filter string="男员工" name="gender_male"
domain="[('gender', '=', 'male')]"/>
<filter string="女员工" name="gender_female"
domain="[('gender', '=', 'female')]"/>
<separator/>
<!--分组-->
<group expand="0" string="分组">
<filter name="group_leader" string="领导" domain="[]" context="{'group_by':'leader_id'}"/>
<filter name="group_company" string="Company" domain="[]" context="{'group_by':'company_id'}"
groups="base.group_multi_company"/>
</group>
</search>
</field>
</record>
我们可以看到:
1、<search></search>包裹表示为search视图
2、<field name="XXX" />声明可以用于搜索的字段
3、<filter string="XXX" name="XXX" domain="XXX" />表示系统定义的过滤器
4、使用<group></group>包裹filter可以进行分组
实际效果如下:
1)搜索
2)过滤器
3)分组
5、编写动作和菜单
我们写好了tree、form和search视图,我们需要编写动作和菜单来定义行为:
点击菜单->触发菜单对应的action动作->展示action中绑定的视图
我们继续在data内增加:
<record model="ir.actions.act_window" id="view_ml_employee_action">
<field name="name">员工信息</field>
<field name="res_model">ml.employee</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_ml_employee_tree"/>
<field name="search_view_id" ref="view_ml_employee_filter"/>
</record>
我们可以看到,动作对应的系统model为ir.action.act_window,我们一样可以在技术->动作->动作下找到我们定义的动作。
view_mode:表示我们需要展示的视图(有先后顺序),tree视图在最前面,我们触发动作时首先展示的就是tree视图;
view_id:表示我们引用的视图ref="view_ml_employee_tree",也就是我们在前面定义的tree视图;
search_view_id:表示我们引用的过滤器为"view_ml_employee_filter";
根据上面需要引用tree和search我们不难推断,可以为model定义多个tree、form和search视图,通过不同的action,绑定不同的菜单,可以触发同一模型不同的展示视图。
最后,我们为action添加一个菜单,我们习惯于将菜单使用单独的文件保存,所以我们新建views/menu.xml文件,书写下列内容:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!--一级菜单-->
<menuitem
id="menu_employee_root"
name="员工"
web_icon="hr,static/description/icon.png"
sequence="1"/>
<!--二级菜单 -->
<menuitem
id="menu_employee_info"
name="员工信息"
parent="menu_employee_root"
sequence="1"/>
<!--三级菜单 -->
<menuitem
id="menu_view_ml_employee_tree"
name="员工档案"
action="view_ml_employee_action"
parent="menu_employee_info"
sequence="1"/>
</data>
</odoo>
可以看到,我们使用menuitem标签定义菜单:
web_icon::一级菜单特有属性,表示展示的图标,这里我们借用hr模块的图标
sequence:菜单的展示顺序
parent:上级菜单,没有定义则为一级菜单
action:菜单对应的动作,我们在三级菜单中添加我们刚才编写的action:view_ml_employee_action
6、引用并安装模块
在__manifest__.py->data中引用views:
'data': [
'views/employee.xml',
'views/menu.xml',
],
然后我们重启服务器,再打开“开发者模式”,在应用页面中刷新本地列表
再搜索employee,点击安装
安装之后我们就可以看到菜单了:
撒花!!!等等!看不到?为什么呢!
Tips:Odoo12之前,admin用户就是root用户。Odoo12新增了root用户,在用户列表中不显示,只在框架需要使用sudo增加权限时才使用。admin依然可以登入系统并拥有所有功能的访问权限,但不再能绕过访问限制。
那我们有没有办法进入root用户模式呢?有的:
首先,我们登入admin用户,然后"激活开发者模式",右上角进行"登出",你会发现登录页面多了一个"以超级用户登录"的方式
点击登录进去发现右上角有花纹,代表已经进入root模式,此时发现已经可以看到"员工模块"信息。
但是我们不能每次都通过这种方式来访问,而且其他用户也没有办法对这个页面进行访问,所以我们要为它写访问权限:
新建employee/security目录,在security目录下新建ir.model.access.csv文件,增加内容:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ml_employee,员工档案权限,employee.model_ml_employee,,1,1,1,1
这样查看比较混乱,我们再pycharm文件内->右键->edit as table->确定:
id:唯一标识
name:名称
model_id:id:对应model,使用model_+下划线格式模型_name作为标识
group_id:id:所属群组信息,这里我们置空
perm_read、perm_write、perm_create、perm_unlink分别为读、写、创、删四个权限
在manifest中加上引用:
'data': [
'security/ir.model.access.csv', 'views/employee.xml',
'views/menu.xml',
]
在应用界面中升级应用,ok~
参考
1、Odoo官网引导: http://www.odoo.com/documentation/12.0/reference/guidelines.html
2、《Odoo12 Development Essentials --Fourth Edition》 --Daniel Reis
声明
原文来自于博客园(https://www.cnblogs.com/ljwTiey/p/11486885.html)
转载请注明文章出处,文章如有任何版权问题,请联系作者删除。
有任何问题,联系邮箱:26476395@qq.com
odoo12从零开始:三、1)创建你的第一个应用模型(module)的更多相关文章
- odoo12从零开始:三、2)odoo模型层
前言 上一篇文章(创建你的第一个应用模块(module))已经大致描述了odoo的模型层(model)和视图层(view),这一篇文章,我们将系统地介绍有关于model的知识,其中包括: 1.模型的类 ...
- Django多对多表的三种创建方式,MTV与MVC概念
MTV与MVC MTV模型(django): M:模型层(models.py) T:templates V:views MVC模型: M:模型层(models.py) V:视图层(views.py) ...
- Java 数组的三种创建方法
public static void main(String[] args) { //创建数组的第一种方法 int[] arr=new int[6]; int intValue=arr[5]; //S ...
- 【C语言探索之旅】 第三课:你的第一个程序
内容简介 1.课程大纲 2.第一部分第三课:你的第一个程序 3.第一部分第四课预告:变量的世界 课程大纲 我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案.还会带大家用C语言编写三个 ...
- Java 9 揭秘(3. 创建你的第一个模块)
文 by / 林本托 Tips 做一个终身学习的人. 在这个章节中,主要介绍以下内容: 如何编写模块化的Java程序 如何编译模块化程序 如何将模块的项目打包成模块化的JAR文件 如何运行模块化程序 ...
- Struts2之命名空间与Action的三种创建方式
看到上面的标题,相信大家已经知道我们接下来要探讨的知识了,一共两点:1.package命名空间设置:2.三种Action的创建方式.下面我们开始本篇的内容: 首先我们聊一聊命名空间的知识,namesp ...
- Js基础知识4-函数的三种创建、四种调用(及关于new function()的解释)
在js中,函数本身属于对象的一种,因此可以定义.赋值,作为对象的属性或者成为其他函数的参数.函数名只是函数这个对象类的引用. 函数定义 // 函数的三种创建方法(定义方式) function one( ...
- Java 数组的三种创建方法,数组拷贝方法
public static void main(String[] args) {//创建数组的第一种方法int[] arr=new int[6];int intValue=arr[5];//Syste ...
- JavaScript 闭包的详细分享(三种创建方式)(附小实例)
JavaScript闭包的详细理解 一.原理:闭包函数--指有权访问私有函数里面的变量和对象还有方法等:通俗的讲就是突破私有函数的作用域,让函数外面能够使用函数里面的变量及方法. 1.第一种创建方式 ...
随机推荐
- Windows的 IIS 部署django项目
Windows的 IIS 部署django项目 1.安装Windows的IIS 功能(win10为例): (1)进入控制面板 :选择大图标 进入程序和功能 (2)启用或者关闭Windows功能 ...
- Unittest 支持 case 失败后自动截图功能的另外两种方式
原生的unittest框架是不支持case失败后自动截图的功能的,网上看了大家的解决办法,大体上分为两种:1.要么加装饰器2.也有人封装断言这里我们看看还有没有其他的更加方便的方法值得大家一起探讨一下 ...
- 利用cookie实现浏览器中多个标签页之间的通信
原理: cookie是浏览器端的存储容器,而且它是多页面共享的,利用cookie多页面共享的特性,可以实现多个标签页的通信. 比如: 一个标签页发送消息(将发送的消息设置到cookie中),一个标签页 ...
- shell 提取文件名和目录名
转自http://blog.csdn.net/universe_hao/article/details/52640321 shell 提取文件名和目录名 在写shell脚本中,经常会有需要对路径和文件 ...
- centos7 yum搭建lnmp环境及配置wordpress超详细教程
yum安装lnmp环境是最方便,最快捷的一种方法.源码编译安装需要花费大量的人类时间,当然源码编译可以个性化配置一些其它功能.目前来说,yum安装基本满足我们搭建web服务器的需求. 本文是我根据近期 ...
- jmeter之beanshell使用
beanshell官网:http://www.BeanShell.org/ 一.beanshell介绍 是一种完全符合Java语法规范的轻量级的脚本语言: 相当于一个小巧免费嵌入式的Java源代码解释 ...
- net core Webapi基础工程搭建(六)——数据库操作_Part 1
目录 前言 SqlSugar Service层 BaseService(基类) 小结 前言 后端开发最常打交道的就是数据库了(静态网站靠边),上一篇net core Webapi基础工程搭建(五)-- ...
- html页面中关于按钮type的要求
重要事项:如果在 HTML 表单中使用 button 元素,不同的浏览器会提交不同的值.Internet Explorer 将提交 <button> 与 </button> 之 ...
- 分布式任务队列--Celery的学习笔记
一.Celery简介 Celery是一个简单,灵活,可靠的分布式系统,用于处理大量消息,同时为操作提供维护此类系统所需的工具.它是一个任务队列,专注于实时处理,同时还支持任务调度. 所谓任务队列,是一 ...
- Mina各组件介绍
Mina各组件介绍 上一篇文章已经系统的介绍了Mina的运行流程,Apache推出的Mina性能上很是高效,上章节我们知道内部有很多的类,各个类之间的依赖也是很多,他们之家都是相互依赖. 下面主要看看 ...