第18讲、Odoo接口开发详解:原理、类型与实践

发布于:2025-06-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

1. 引言

Odoo作为一个功能强大的开源ERP和业务应用套件,其开放性和可扩展性是核心优势之一。接口(API)开发在Odoo生态中扮演着至关重要的角色,它使得Odoo能够与外部系统、第三方应用、移动端以及Web前端进行数据交换和功能集成。理解Odoo的接口开发机制,对于实现复杂的业务集成、构建定制化解决方案至关重要。本文档将深入探讨Odoo接口开发的实现原理,区分不同的接口类型,并提供实践指导。

2. Odoo接口开发原理与技术架构

Odoo的接口开发主要基于其稳健的Web框架和模型驱动的架构。其核心原理可以概括为以下几点:

2.1 客户端-服务器架构

Odoo采用标准的客户端-服务器(Client-Server)架构。客户端(通常是Web浏览器,也可以是其他应用程序)通过网络向Odoo服务器发送请求,服务器处理请求后返回响应。这种架构使得Odoo的功能可以通过网络被远程访问和调用。

2.2 RPC(远程过程调用)

Odoo原生支持RPC机制,允许外部应用程序像调用本地函数一样调用Odoo服务器上的方法。这是实现外部系统与Odoo交互的基础。Odoo主要支持两种RPC协议:XML-RPC和JSON-RPC。这两种协议都允许通过HTTP(S)传输结构化的请求和响应数据。

2.3 Web控制器(Controllers)

除了标准的RPC接口,Odoo还提供了一个强大的Web控制器层。开发者可以通过创建自定义控制器来定义HTTP路由(Routes),处理HTTP请求(GET, POST, PUT, DELETE等),并返回各种格式的响应(HTML, JSON, 文件等)。这使得开发者能够构建灵活的、符合RESTful风格的API,或者实现自定义的Web页面和交互逻辑。

2.4 模型驱动

无论是通过RPC还是控制器,对Odoo数据的操作最终都会落实到Odoo的模型(Models)层。Odoo的模型定义了数据结构和业务逻辑。接口调用通常会触发模型方法的执行,如create(), search_read(), write(), unlink()等,从而实现对数据的增删改查。

3. Odoo接口类型详解

Odoo提供了多种接口开发方式,以适应不同的集成需求和技术偏好。主要可以分为以下几类:

3.1 XML-RPC接口

XML-RPC是一种基于XML的RPC协议,允许通过HTTP发送方法调用请求。Odoo通过/xmlrpc/2/common(用于认证和数据库信息)和/xmlrpc/2/object(用于调用模型方法)两个端点提供XML-RPC服务。

原理: 客户端将方法调用信息(方法名、参数)编码为XML格式,通过HTTP POST请求发送到Odoo服务器的指定端点。服务器解析XML请求,执行相应的方法,并将结果编码为XML格式返回给客户端。

适用场景: 主要用于需要与多种异构系统(特别是那些原生支持XML-RPC的系统)进行集成的场景。虽然相对古老,但仍然是一种稳定可靠的集成方式。

实现示例 (Python):

import xmlrpc.client

url = "http://localhost:8069"  # Odoo服务器地址
db = "your_database_name"      # Odoo数据库名
username = "admin"              # Odoo用户名
password = "your_password"      # Odoo密码

# 1. 认证 获取用户ID (uid)
try:
    common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
    uid = common.authenticate(db, username, password, {})
    if not uid:
        print("认证失败!")
        exit()
    print(f"认证成功, UID: {uid}")
except Exception as e:
    print(f"连接或认证错误: {e}")
    exit()

# 2. 调用模型方法 (以查询合作伙伴为例)
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')

try:
    partner_ids = models.execute_kw(db, uid, password,
                                    'res.partner',  # 模型名
                                    'search',       # 方法名
                                    [[['is_company', '=', True]]]) # 参数 (domain)
    print(f"找到的公司数量: {len(partner_ids)}")

    if partner_ids:
        partners_data = models.execute_kw(db, uid, password,
                                          'res.partner',
                                          'read',         # 方法名
                                          [partner_ids],  # 参数 (ids)
                                          {'fields': ['name', 'email', 'phone']}) # 可选参数 (指定字段)
        print("合作伙伴信息:")
        for partner in partners_data:
            print(partner)

except xmlrpc.client.Fault as error:
    print(f"XML-RPC 调用错误: {error}")
except Exception as e:
    print(f"执行模型方法时出错: {e}")

优缺点:

  • 优点:标准化协议,跨语言支持良好,稳定。
  • 缺点:基于XML,数据传输相对冗余;相比JSON-RPC和REST,性能可能稍差。

3.2 JSON-RPC接口

JSON-RPC是一种基于JSON的轻量级RPC协议。Odoo通过/jsonrpc端点提供JSON-RPC服务。

原理: 客户端将方法调用信息编码为JSON格式,通过HTTP POST请求发送到Odoo服务器的/jsonrpc端点。请求体是一个包含jsonrpc, method, params, id等字段的JSON对象。服务器解析JSON请求,执行相应的方法,并将结果编码为JSON格式返回。

适用场景: 广泛用于Web前端(JavaScript可以直接处理JSON)、移动应用以及需要轻量级数据交换的现代系统集成。

实现示例 (请求格式):

POST /jsonrpc HTTP/1.1
Host: localhost:8069
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "call", // 固定为 'call'
  "params": {
    "service": "object", // 'common' 用于认证, 'object' 用于模型方法
    "method": "execute_kw", // 调用模型方法的标准方法
    "args": [
      "your_database_name", // 数据库名
      1, // 用户UID (认证后获得)
      "your_password", // 用户密码
      "res.partner", // 目标模型
      "search_read", // 要调用的模型方法
      [[["is_company", "=", true]]], // 方法的位置参数 (domain)
      {"fields": ["name", "email"], "limit": 5} // 方法的关键字参数
    ]
  },
  "id": 1 // 请求ID,用于匹配响应
}

Python实现示例:

import requests
import json

url = "http://localhost:8069/jsonrpc"
db = "your_database_name"
username = "admin"
password = "your_password"

# 1. 认证获取UID
payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "service": "common",
        "method": "authenticate",
        "args": [db, username, password, {}]
    },
    "id": 1
}

response = requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"})
result = response.json()
if "error" in result:
    print(f"认证错误: {result['error']}")
    exit()

uid = result["result"]
print(f"认证成功, UID: {uid}")

# 2. 调用模型方法
payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "service": "object",
        "method": "execute_kw",
        "args": [
            db, uid, password,
            "res.partner",
            "search_read",
            [[["is_company", "=", True]]],
            {"fields": ["name", "email", "phone"], "limit": 5}
        ]
    },
    "id": 2
}

response = requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"})
result = response.json()
if "error" in result:
    print(f"调用错误: {result['error']}")
else:
    print("合作伙伴信息:")
    for partner in result["result"]:
        print(partner)

优缺点:

  • 优点:基于JSON,轻量、易于解析,性能较好,特别适合Web和移动端开发。
  • 缺点:虽然是标准,但相比RESTful API在语义表达上可能不够直观。

3.3 自定义控制器 (RESTful API)

通过编写自定义控制器,开发者可以创建完全符合REST(Representational State Transfer)架构风格的API。RESTful API使用标准的HTTP方法(GET, POST, PUT, DELETE等)对资源进行操作,并通过URL路径标识资源。

原理: 开发者在Odoo模块的controllers目录下创建Python文件,定义继承自odoo.http.Controller的类。在类中使用@http.route()装饰器来定义路由规则,包括URL路径、允许的HTTP方法、认证方式 (auth)、响应类型 (type)等。控制器方法内部可以使用request对象访问请求信息(如参数、头信息)和Odoo环境 (request.env) 来调用模型方法或执行其他业务逻辑,最后构造并返回HTTP响应。

适用场景: 构建现代Web应用、移动应用的后端API;需要对API结构、请求/响应格式、认证方式有完全控制权的场景;希望提供更直观、语义化的API接口。

实现示例 (controllers/main.py):

from odoo import http
from odoo.http import request, Response
import json

class PartnerApiController(http.Controller):

    # 获取合作伙伴列表 (JSON响应)
    @http.route('/api/v1/partners', type='json', auth='user', methods=['GET'], csrf=False)
    def get_partners_json(self, limit=10, offset=0, domain=None, **kwargs):
        """ 获取合作伙伴列表 (需要用户认证) """
        try:
            # 考虑安全性,对传入的domain进行处理或限制
            search_domain = [('customer_rank', '>', 0)] # 示例:只获取客户
            if domain and isinstance(domain, list):
                # 注意:直接使用外部传入的domain可能存在安全风险,需要校验
                # search_domain.extend(domain)
                pass # 实际应用中应谨慎处理外部domain

            partners = request.env['res.partner'].search_read(
                domain=search_domain,
                fields=['id', 'name', 'email', 'phone'],
                limit=int(limit),
                offset=int(offset)
            )
            total = request.env['res.partner'].search_count(search_domain)
            return {'status': 'success', 'total': total, 'data': partners}
        except Exception as e:
            # 记录错误日志
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 获取单个合作伙伴详情 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='http', auth='user', methods=['GET'], csrf=False)
    def get_partner_http(self, partner_id, **kwargs):
        """ 获取单个合作伙伴详情 (需要用户认证) """
        partner = request.env['res.partner'].search_read(
            domain=[('id', '=', partner_id)],
            fields=['id', 'name', 'email', 'phone', 'street', 'city', 'country_id']
        )
        if not partner:
            return Response(json.dumps({'status': 'error', 'message': 'Partner not found'}), status=404, content_type='application/json')

        # 返回JSON格式的HTTP响应
        return Response(json.dumps({'status': 'success', 'data': partner[0]}), content_type='application/json')

    # 创建合作伙伴 (JSON响应)
    @http.route('/api/v1/partners', type='json', auth='user', methods=['POST'], csrf=False)
    def create_partner(self, **kwargs):
        """ 创建新的合作伙伴 (需要用户认证) """
        required_fields = ['name']
        missing_fields = [f for f in required_fields if f not in kwargs]
        if missing_fields:
            return {'status': 'error', 'message': f'Missing required fields: {missing_fields}'}

        try:
            # 提取请求数据
            partner_data = {
                'name': kwargs.get('name'),
                'email': kwargs.get('email'),
                'phone': kwargs.get('phone'),
                'street': kwargs.get('street'),
                'city': kwargs.get('city'),
                'customer_rank': kwargs.get('is_customer') and 1 or 0,
            }
            
            # 创建记录
            partner = request.env['res.partner'].create(partner_data)
            return {
                'status': 'success',
                'message': 'Partner created successfully',
                'id': partner.id
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 更新合作伙伴 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='json', auth='user', methods=['PUT'], csrf=False)
    def update_partner(self, partner_id, **kwargs):
        """ 更新合作伙伴信息 (需要用户认证) """
        partner = request.env['res.partner'].browse(partner_id)
        if not partner.exists():
            return {'status': 'error', 'message': 'Partner not found'}

        try:
            # 提取要更新的字段
            update_data = {}
            for field in ['name', 'email', 'phone', 'street', 'city']:
                if field in kwargs:
                    update_data[field] = kwargs[field]
            
            if 'is_customer' in kwargs:
                update_data['customer_rank'] = kwargs['is_customer'] and 1 or 0
            
            # 更新记录
            partner.write(update_data)
            return {
                'status': 'success',
                'message': 'Partner updated successfully'
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 删除合作伙伴 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='json', auth='user', methods=['DELETE'], csrf=False)
    def delete_partner(self, partner_id, **kwargs):
        """ 删除合作伙伴 (需要用户认证) """
        partner = request.env['res.partner'].browse(partner_id)
        if not partner.exists():
            return {'status': 'error', 'message': 'Partner not found'}

        try:
            # 删除记录
            partner.unlink()
            return {
                'status': 'success',
                'message': 'Partner deleted successfully'
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

优缺点:

  • 优点:完全可定制,符合现代API设计标准,语义清晰,可以精确控制请求/响应格式和认证方式。
  • 缺点:需要更多的代码编写,相比直接使用RPC接口,开发工作量较大。

4. 接口认证与安全

无论使用哪种接口类型,安全性都是一个关键考量。Odoo提供了多种认证和安全机制:

4.1 基本认证方式

  1. 用户名/密码认证:最基本的认证方式,适用于XML-RPC和JSON-RPC。
  2. Session认证:基于Cookie的会话认证,主要用于Web界面和控制器。
  3. API密钥认证:通过自定义控制器实现,更适合系统间集成。
  4. OAuth2认证:可以通过第三方模块实现,支持更复杂的授权流程。

4.2 控制器认证参数

在自定义控制器中,@http.route()装饰器的auth参数控制认证方式:

  • auth='none':无需认证,任何人都可以访问(适用于公开API)。
  • auth='public':允许未登录用户访问,但会创建一个公共(匿名)用户会话。
  • auth='user':需要登录用户才能访问(默认值)。
  • auth='admin':仅管理员可以访问。

4.3 安全最佳实践

  1. CSRF保护:对于需要修改数据的请求,Odoo默认启用CSRF保护。如果是外部API调用,可以通过设置csrf=False来禁用。
  2. 数据验证:始终验证和清理输入数据,特别是来自外部的请求参数。
  3. 权限控制:利用Odoo的访问控制列表(ACL)和记录规则来限制数据访问。
  4. 使用sudo()谨慎sudo()方法可以绕过权限检查,应谨慎使用,并确保在必要时恢复正常环境。
  5. API速率限制:考虑实现API调用速率限制,防止滥用。
  6. 日志记录:记录关键API调用,便于审计和故障排查。

4.4 JWT认证实现示例

JWT(JSON Web Token)是一种流行的API认证方式,特别适合无状态的RESTful API。以下是在Odoo中实现JWT认证的简化示例:

# 需要安装PyJWT库: pip install PyJWT
import jwt
import datetime
from odoo import http
from odoo.http import request, Response
import json

SECRET_KEY = "your-secret-key"  # 实际应用中应存储在安全的配置中

class JWTAuthController(http.Controller):
    
    @http.route('/api/auth/token', type='json', auth='none', methods=['POST'], csrf=False)
    def get_token(self, **kwargs):
        """获取JWT令牌的接口"""
        username = kwargs.get('username')
        password = kwargs.get('password')
        
        if not username or not password:
            return {'status': 'error', 'message': 'Missing credentials'}
        
        # 验证用户凭据
        uid = request.env['res.users'].sudo().authenticate(request.env.cr.dbname, username, password, {})
        if not uid:
            return {'status': 'error', 'message': 'Invalid credentials'}
        
        # 生成JWT令牌
        payload = {
            'uid': uid,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),  # 1天过期
            'iat': datetime.datetime.utcnow(),
        }
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
        
        return {'status': 'success', 'token': token}
    
    @staticmethod
    def validate_token(token):
        """验证JWT令牌"""
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            return payload
        except jwt.ExpiredSignatureError:
            return None
        except jwt.InvalidTokenError:
            return None

# 使用JWT保护的API示例
class ProtectedApiController(http.Controller):
    
    @http.route('/api/protected/data', type='http', auth='none', methods=['GET'], csrf=False)
    def get_protected_data(self, **kwargs):
        """受JWT保护的API端点"""
        # 从请求头获取令牌
        auth_header = request.httprequest.headers.get('Authorization')
        if not auth_header or not auth_header.startswith('Bearer '):
            return Response(json.dumps({'status': 'error', 'message': 'No token provided'}), 
                           status=401, content_type='application/json')
        
        token = auth_header.split(' ')[1]
        payload = JWTAuthController.validate_token(token)
        
        if not payload:
            return Response(json.dumps({'status': 'error', 'message': 'Invalid or expired token'}), 
                           status=401, content_type='application/json')
        
        # 使用令牌中的用户ID设置环境
        uid = payload.get('uid')
        try:
            # 以特定用户身份执行操作
            user = request.env['res.users'].sudo().browse(uid)
            if not user.exists():
                return Response(json.dumps({'status': 'error', 'message': 'User not found'}), 
                               status=401, content_type='application/json')
            
            # 切换到该用户的环境
            env = request.env(user=uid)
            
            # 执行受保护的操作
            data = env['res.partner'].search_read(
                domain=[('customer_rank', '>', 0)],
                fields=['name', 'email'],
                limit=5
            )
            
            return Response(json.dumps({'status': 'success', 'data': data}), 
                           content_type='application/json')
        except Exception as e:
            return Response(json.dumps({'status': 'error', 'message': str(e)}), 
                           status=500, content_type='application/json')

5. 接口的组织与最佳实践

5.1 模块结构

一个标准的Odoo API模块结构如下:

my_api_module/
├── __init__.py              # 导入controllers和models
├── __manifest__.py          # 模块清单
├── controllers/
│   ├── __init__.py          # 导入控制器
│   ├── main.py              # 主控制器
│   └── auth.py              # 认证控制器
├── models/
│   ├── __init__.py          # 导入模型
│   └── api_log.py           # API日志模型
├── security/
│   └── ir.model.access.csv  # 访问权限
└── static/
    └── description/
        └── icon.png         # 模块图标

5.2 API版本控制

在URL路径中包含版本号是一种良好的实践,例如/api/v1/partners。这样在API变更时,可以保持向后兼容性。

# v1版本API
@http.route('/api/v1/partners', ...)
def get_partners_v1(self, **kwargs):
    # v1实现
    pass

# v2版本API(新增功能或改变行为)
@http.route('/api/v2/partners', ...)
def get_partners_v2(self, **kwargs):
    # v2实现
    pass

5.3 响应格式标准化

保持一致的响应格式有助于客户端处理:

def standard_response(success=True, data=None, message=None, status_code=200):
    """生成标准化的API响应"""
    response = {
        'status': 'success' if success else 'error',
    }
    
    if data is not None:
        response['data'] = data
    
    if message:
        response['message'] = message
    
    if not success and status_code == 200:
        status_code = 400  # 默认错误状态码
    
    return Response(json.dumps(response), status=status_code, content_type='application/json')

5.4 API日志记录

记录API调用对于调试和审计非常有用:

def log_api_call(self, endpoint, request_data, response_data, success, user_id=None):
    """记录API调用"""
    try:
        log_data = {
            'name': endpoint,
            'request_data': json.dumps(request_data),
            'response_data': json.dumps(response_data),
            'success': success,
            'user_id': user_id or request.env.uid,
            'ip_address': request.httprequest.remote_addr,
        }
        request.env['api.log'].sudo().create(log_data)
    except Exception as e:
        _logger.error(f"Failed to log API call: {e}")

5.5 分页、过滤和排序

对于返回大量数据的API,应实现分页、过滤和排序功能:

@http.route('/api/v1/partners', type='http', auth='user', methods=['GET'], csrf=False)
def get_partners_paginated(self, **kwargs):
    # 分页参数
    limit = int(kwargs.get('limit', 10))
    offset = int(kwargs.get('offset', 0))
    
    # 过滤参数
    filters = {}
    for key in ['name', 'email', 'country_id']:
        if key in kwargs:
            filters[key] = kwargs[key]
    
    # 构建domain
    domain = []
    for key, value in filters.items():
        if key == 'name' or key == 'email':
            domain.append((key, 'ilike', value))
        else:
            domain.append((key, '=', int(value)))
    
    # 排序参数
    order = kwargs.get('order', 'name asc')
    
    # 查询数据
    partners = request.env['res.partner'].search_read(
        domain=domain,
        fields=['name', 'email', 'phone', 'country_id'],
        limit=limit,
        offset=offset,
        order=order
    )
    
    # 获取总记录数(用于分页)
    total = request.env['res.partner'].search_count(domain)
    
    # 构建响应
    response = {
        'status': 'success',
        'data': partners,
        'pagination': {
            'total': total,
            'limit': limit,
            'offset': offset,
            'pages': (total + limit - 1) // limit
        }
    }
    
    return Response(json.dumps(response), content_type='application/json')

6. Odoo 18的接口增强

Odoo 18版本在接口开发方面引入了一些增强功能:

6.1 HTTP方法支持扩展

@http.route装饰器现在支持更多HTTP方法的限定,包括PUTDELETEPATCH等,使得开发RESTful API更加便捷。

@http.route('/api/resource', methods=['PATCH'], ...)
def update_resource_partial(self, **kwargs):
    # 处理PATCH请求(部分更新)
    pass

6.2 请求头处理增强

可以更方便地访问和处理HTTP请求头:

@http.route('/api/headers', type='http', auth='none', csrf=False)
def handle_headers(self, **kwargs):
    # 获取请求头
    headers = request.httprequest.headers
    user_agent = headers.get('User-Agent')
    content_type = headers.get('Content-Type')
    
    # 设置响应头
    response = Response(json.dumps({'status': 'success'}), content_type='application/json')
    response.headers['X-Custom-Header'] = 'Custom Value'
    
    return response

6.3 JSON请求处理改进

Odoo 18改进了对嵌套JSON数据的处理:

@http.route('/api/complex-data', type='json', auth='none', csrf=False)
def handle_complex_json(self, **kwargs):
    # 直接访问嵌套的JSON数据
    nested_data = kwargs.get('nested', {})
    value = nested_data.get('deep', {}).get('value')
    
    return {'received_value': value}

6.4 Webhook支持

使用auth='none'可以轻松创建外部系统可访问的webhook端点:

@http.route('/webhook/payment-notification', type='json', auth='none', csrf=False)
def payment_webhook(self, **kwargs):
    # 验证webhook签名
    signature = request.httprequest.headers.get('X-Webhook-Signature')
    if not self._validate_webhook_signature(signature, request.httprequest.data):
        return {'status': 'error', 'message': 'Invalid signature'}
    
    # 处理webhook数据
    payment_data = kwargs
    # ... 处理支付通知 ...
    
    return {'status': 'success'}

7. 实际应用场景

7.1 与外部系统集成

7.1.1 ERP系统集成
# 在外部ERP系统中调用Odoo API
def sync_products_from_odoo():
    # 使用XML-RPC连接Odoo
    common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
    uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
    models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
    
    # 获取产品数据
    products = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
                                'product.product', 'search_read',
                                [[['sale_ok', '=', True]]],
                                {'fields': ['name', 'list_price', 'default_code', 'barcode']})
    
    # 同步到本地数据库
    for product in products:
        update_local_product(product)
7.1.2 电子商务集成
# Odoo控制器处理来自电商平台的订单
@http.route('/api/v1/orders', type='json', auth='api_key', methods=['POST'], csrf=False)
def receive_ecommerce_order(self, **kwargs):
    # 验证API密钥
    api_key = request.httprequest.headers.get('X-API-Key')
    if not self._validate_api_key(api_key):
        return {'status': 'error', 'message': 'Invalid API key'}
    
    # 创建销售订单
    try:
        order_data = kwargs.get('order', {})
        customer_data = order_data.get('customer', {})
        
        # 查找或创建客户
        customer = self._find_or_create_customer(customer_data)
        
        # 创建订单头
        order_vals = {
            'partner_id': customer.id,
            'date_order': fields.Datetime.now(),
            'external_id': order_data.get('external_id'),
            'note': order_data.get('note'),
        }
        order = request.env['sale.order'].sudo().create(order_vals)
        
        # 创建订单行
        for line in order_data.get('lines', []):
            product = request.env['product.product'].sudo().search([
                ('default_code', '=', line.get('sku'))
            ], limit=1)
            
            if product:
                line_vals = {
                    'order_id': order.id,
                    'product_id': product.id,
                    'product_uom_qty': line.get('quantity', 1.0),
                    'price_unit': line.get('price_unit'),
                }
                request.env['sale.order.line'].sudo().create(line_vals)
        
        # 确认订单
        if order_data.get('confirm', False):
            order.action_confirm()
        
        return {
            'status': 'success',
            'message': 'Order created successfully',
            'order_id': order.id,
            'name': order.name
        }
    except Exception as e:
        request.env.cr.rollback()
        _logger.error(f"Failed to create order: {e}")
        return {'status': 'error', 'message': str(e)}

7.2 移动应用后端

# 为移动应用提供认证API
@http.route('/api/v1/mobile/login', type='json', auth='none', methods=['POST'], csrf=False)
def mobile_login(self, **kwargs):
    username = kwargs.get('username')
    password = kwargs.get('password')
    
    if not username or not password:
        return {'status': 'error', 'message': 'Missing credentials'}
    
    try:
        uid = request.env['res.users'].sudo().authenticate(request.env.cr.dbname, username, password, {})
        if not uid:
            return {'status': 'error', 'message': 'Invalid credentials'}
        
        user = request.env['res.users'].sudo().browse(uid)
        
        # 生成访问令牌
        token_data = {
            'uid': uid,
            'name': user.name,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30),
        }
        token = self._generate_token(token_data)
        
        # 返回用户信息和令牌
        return {
            'status': 'success',
            'token': token,
            'user': {
                'id': user.id,
                'name': user.name,
                'email': user.email,
                'image': user.image_128 and user.image_128.decode('utf-8') or False,
            }
        }
    except Exception as e:
        _logger.error(f"Mobile login error: {e}")
        return {'status': 'error', 'message': 'Login failed'}

7.3 第三方服务集成

# 集成支付网关回调
@http.route('/api/payment/callback', type='http', auth='none', methods=['POST'], csrf=False)
def payment_gateway_callback(self, **kwargs):
    # 验证回调请求
    signature = request.httprequest.headers.get('X-Signature')
    if not self._verify_payment_signature(signature, kwargs):
        return Response("Invalid signature", status=400)
    
    # 处理支付结果
    payment_reference = kwargs.get('reference')
    status = kwargs.get('status')
    amount = float(kwargs.get('amount', '0.0'))
    
    try:
        # 查找相关订单
        order = request.env['sale.order'].sudo().search([
            ('payment_reference', '=', payment_reference)
        ], limit=1)
        
        if not order:
            return Response("Order not found", status=404)
        
        # 更新订单状态
        if status == 'paid':
            # 创建支付记录
            payment_vals = {
                'amount': amount,
                'payment_type': 'inbound',
                'partner_type': 'customer',
                'partner_id': order.partner_id.id,
                'communication': f"Payment for {order.name}",
                # 其他必要字段...
            }
            payment = request.env['account.payment'].sudo().create(payment_vals)
            
            # 确认支付
            payment.action_post()
            
            # 更新订单
            order.write({'payment_status': 'paid'})
            
            # 如果订单未确认,则确认订单
            if order.state == 'draft':
                order.action_confirm()
            
            return Response("Payment processed successfully", status=200)
        else:
            order.write({'payment_status': status})
            return Response("Payment status updated", status=200)
    except Exception as e:
        request.env.cr.rollback()
        _logger.error(f"Payment callback error: {e}")
        return Response(f"Error processing payment: {e}", status=500)

8. 接口测试与调试

8.1 使用Postman测试

Postman是测试API的常用工具。以下是测试Odoo API的基本步骤:

  1. 设置环境变量

    • odoo_url: Odoo服务器URL
    • db_name: 数据库名称
    • username: 用户名
    • password: 密码
  2. 创建认证请求

    • 对于XML-RPC,使用Pre-request Script转换请求
    • 对于JSON-RPC,直接发送JSON请求
    • 对于自定义API,根据认证方式设置
  3. 保存认证响应

    • 使用Tests脚本保存认证令牌或UID
  4. 创建API请求集合

    • 组织不同的API请求
    • 使用保存的认证信息

8.2 日志调试

在Odoo服务器端启用调试日志:

import logging
_logger = logging.getLogger(__name__)

@http.route('/api/debug', ...)
def debug_api(self, **kwargs):
    _logger.info("Request received: %s", kwargs)
    _logger.debug("Request headers: %s", request.httprequest.headers)
    
    # 处理请求...
    
    _logger.info("Response sent: %s", response)
    return response

8.3 使用OpenAPI/Swagger文档

通过第三方模块(如rest_api_documentation),可以为Odoo API生成Swagger文档:

# 在控制器方法中添加OpenAPI文档注释
@http.route('/api/v1/partners', ...)
def get_partners(self, **kwargs):
    """
    @api {get} /api/v1/partners 获取合作伙伴列表
    @apiName GetPartners
    @apiGroup Partner
    @apiVersion 1.0.0
    
    @apiParam {Number} [limit=10] 每页记录数
    @apiParam {Number} [offset=0] 偏移量
    @apiParam {String} [name] 按名称过滤
    
    @apiSuccess {String} status 请求状态
    @apiSuccess {Number} total 总记录数
    @apiSuccess {Object[]} data 合作伙伴列表
    @apiSuccess {Number} data.id 合作伙伴ID
    @apiSuccess {String} data.name 合作伙伴名称
    @apiSuccess {String} data.email 电子邮件
    
    @apiError {String} status 错误状态
    @apiError {String} message 错误消息
    """
    # 实现代码...

9. 总结与最佳实践

9.1 选择合适的接口类型

  • XML-RPC:适用于需要与多种异构系统集成,特别是那些原生支持XML-RPC的系统。
  • JSON-RPC:适用于Web前端、移动应用以及现代系统集成,特别是当性能和数据量是考量因素时。
  • 自定义控制器(RESTful API):适用于需要完全控制API结构、认证方式和响应格式的场景,特别是构建面向外部的API。

9.2 接口设计原则

  1. 一致性:保持URL结构、请求/响应格式的一致性。
  2. 版本控制:在URL中包含版本号,便于API演进。
  3. 资源导向:使用名词(而非动词)来表示资源,如/partners而非/getPartners
  4. HTTP方法语义:正确使用HTTP方法(GET, POST, PUT, DELETE)。
  5. 状态码使用:使用标准HTTP状态码表达请求结果。
  6. 分页与过滤:为返回大量数据的API实现分页和过滤机制。
  7. 错误处理:提供清晰的错误信息和状态码。

9.3 安全最佳实践

  1. 最小权限原则:API只应具有完成其任务所需的最小权限。
  2. 输入验证:始终验证和清理所有输入数据。
  3. HTTPS:始终使用HTTPS加密传输。
  4. 认证与授权:实施强大的认证机制,并确保适当的授权检查。
  5. API密钥轮换:定期轮换API密钥和令牌。
  6. 速率限制:实施API调用速率限制,防止滥用。
  7. 审计日志:记录关键API操作,便于审计和故障排查。

9.4 性能优化

  1. 字段选择:只请求和返回必要的字段。
  2. 批量操作:尽可能使用批量操作而非多次单独调用。
  3. 缓存:适当使用缓存减少数据库查询。
  4. 延迟加载:对于大型数据集,实现分页和延迟加载。
  5. 异步处理:对于耗时操作,考虑异步处理。

通过遵循这些原则和最佳实践,开发者可以创建安全、高效、易于使用的Odoo接口,满足各种集成和扩展需求。无论是构建内部系统集成,还是开发面向外部的API,Odoo的接口开发机制都提供了强大而灵活的支持。


网站公告

今日签到

点亮在社区的每一天
去签到