Odoo16 微信公众号模块开发示例
本模块基于 aiohttp + asyncio 进行异步微信公众号接口开发, 仅实现了部分 API 仅供学习参考,更完善的同步接口请参考:wechatpy 或 werobot,可用来替代 模块中的 wechat client。
业务需求
- 小程序中需要用户先关注公众号后才能进一步操作
- 通过公众号给用户发送通知
功能设计
方案:
同一用户在小程序与公众号中的 openid 是不同的,通过微信 UnionId 机制, 将小程序及公众号绑定到同一开放平台(开放平台需认证缴费),把已有公众号用户同步到后台,并处理关注及取消关注事件,即在后台中同步所有关注用户,并通过 UnionId 判断用户是否已关注,未关注时小程序端展示公众号中已添加的关注引导文章。
功能点:
后端:
- 基于 aiohttp + asyncio 开发公众号 Api 接口
- 公众号基本配置管理
- 关注用户信息管理
- 批量同步公众号关注用户
- 单个用户信息同步
- 微信服务配置回调API
- 关注及取消关注时同步用户信息
- 发送微信模板消息
- 查询用户是否关注接口
小程序端:
- 调用接口判断用户是否已关注
- 未关注时跳转webview打开公众号中关注引导文章
公众号客户端
api/get_user_info/_api.apy
依赖 request.py 封装具体请求参数,解析返回结果client.py
client 实例,注入配置参数,依赖api/*
实现各公众号 API,缓存access_token
在api间重用,进行异常处理,记录日志request.py
封装 aiohttp 客户端请求。
# client.py
class WxClient(object):
'''
基于 aiohttp 的微信公众号异步客户端
使用示例:
wx_client = client_instance(wx_appid, wx_secret)
loop = wx_client.loop
ret = loop.run_until_complete(self.async_get_user_info(self.openid, wx_client))
'''
def __init__(self, app_id, secret):
self._app_id = app_id
self._secret = secret
# access_token 缓存
self._token_store = TokenStore(secret)
# aiohttp 共用异步 http client + 事件循环
self._session, self._loop = init_session()
@handle_exception(bool)
async def get_user_info(self, openid: str) -> get_user_info.WxUserInfo:
token = await self.latest_token()
return await get_user_info.request(self._session, token, openid)
def client_instance(app_id, secret):
'''公众号客户端实例'''
global client
if not client:
client = WxClient(app_id, secret)
return client
# request.py 封装 aiohttp 客户端请求, 初始化 session 、event_loop
def create_session():
conn = aiohttp.TCPConnector(ssl=False)
return aiohttp.ClientSession(connector=conn)
def init_session():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
session = create_session()
return session, loop
def check_response_error(data, error_code=0, error_msg_key='errmsg'):
if code := int(data.get('errcode', 0)) != error_code:
raise ServerError(code, data[error_msg_key])
async def get_response(session, url, params=None, timeout=10, response_callback=None):
async with async_timeout.timeout(timeout):
async with session.get(url, params=params) as response:
if response_callback:
return await response_callback(response)
else:
result = await response.json()
check_response_error(result)
return result
知识点:通过 odoo.tools.config['data_dir']
获取 odoo 数据存储目录,以存储自定义接口日志。
公众号配置
- 公众号配置管理模型
class WxConfig(models.Model):
_name = 'wx.config'
_description = u'公众号配置'
name = fields.Char('名称')
wx_appid = fields.Char('AppId')
wx_secret = fields.Char('AppSecret')
wx_url = fields.Char('URL', readonly=True, compute='_compute_wx_url', help='复制到公众号官方后台')
wx_token = fields.Char('Token', default=_generate_token)
wx_aeskey = fields.Char('EncodingAESKey', default='')
reply = fields.Char(string='关注回复', default='欢迎关注!')
...
知识点:限制模型仅单条记录、展示form视图。
- 通过 data.xml 创建记录
<odoo>
<data noupdate="1">
<record id="wx_config_data_1" model="wx.config">
<field name="name">公众号配置</field>
<field name="wx_appid">xxxxxxx</field>
</record>
</data>
</odoo>
- 视图中 views.xml 创建窗口动作,view_mode 为 form
<record id="list_wx_config_action" model="ir.actions.act_window">
<field name="name">公众号设置</field>
<field name="res_model">wx.config</field>
<field name="view_mode">form</field>
<field name="res_id" ref="wx_config_data_1"/>
</record>
- 将模型权限设置为1100,仅读取修改权限
- 模型中添加模型方法读取该唯一记录:
@api.model
def get_config(self):
return self.env.ref('odooer_wechat.wx_config_data_1')
批量用户同步
- 自定义列表视图,添加同步用户按钮
定义组件模板,添加同步按钮:
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button type="button" class="btn btn-secondary o_button_download_import_tmpl"
t-on-click.stop.prevent="onClickSyncBtn">
同步用户
</button>
</xpath>
组件js, 定义按钮处理逻辑及组件名称 sync_wx_user_tree:
export class SyncUserController extends ListController {
setup() {
super.setup();
this.orm = useService('orm');
}
async onClickSyncBtn() {
const action = await this.orm.call(
"wx.user",
"action_sync_users"
);
window.location.reload();
// this.actionService.doAction(action)
}
}
export const SyncUserListView = {
...listView,
Controller: SyncUserController,
buttonTemplate: "SyncUser.ListView.Buttons",
};
registry.category("views").add("sync_wx_user_tree", SyncUserListView);
在 __manifest__.py
注册自定义组件源码:.js .xml .css
等
'assets': {
'web.assets_backend': [
'odooer_wechat/static/src/components/**/*',
]
},
通过 js_class="sync_wx_user_tree"
指定使用列表自定义组件。
<record id="view_wx_user_list" model="ir.ui.view">
<field name="name">wx.user.list</field>
<field name="model">wx.user</field>
<field name="arch" type="xml">
<tree js_class="sync_wx_user_tree">
.......
- 向前端页面推送消息
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
'title': '同步关注用户信息',
'message': '开始同步公众号关注者信息',
'warning': True
})
- 在线程中处理长时间任务防止界面阻塞等待
thread = threading.Thread(target=_task)
thread.start() # 启动线程执行任务
def _task():
with self.env.registry.cursor() as cr: # 保持使用该游标创建新的环境变量
env = api.Environment(cr, uid, {})
事件处理
将 https://**/wx_handler
复制到微信公众号后台服务配置URL,并启用配置。在 controller 中处理接收到的各种事件及消息。
@http.route('/wx_handler', type='http', auth='none', methods=['GET', 'POST'], csrf=False)
def handle(self, **kwargs):
# 其他...
ret = ''
if msg.type in ['text', 'image', 'voice', 'video', 'location', 'link']:
ret = input_handle(request, msg)
elif msg.type == 'event':
if msg.event == 'subscribe':
ret = subscribe(request, msg)
elif msg.event == 'unsubscribe':
ret = unsubscribe(request, msg)
模块源码
未严格测试,请勿在生产环境直接使用
点击查看 https://apps.odoo.com/apps/modules/16.0/odooer_wechat/
其他参考
- Odoo13 公众号模块:https://apps.odoo.com/apps/modules/13.0/oejia_wx2/
- aiohttp 官网: https://docs.aiohttp.org/en/stable/client.html
- asyncio官网: https://docs.python.org/3/library/asyncio.html