import base64 import urllib.parse from odoo.http import Controller, request, route from datetime import datetime, timedelta from odoo import models, fields, api, http, SUPERUSER_ID, _ import logging, json _logger = logging.getLogger(__name__) def remove_base64_prefix(base64_str: str) -> str: separator_index = base64_str.find(',') if separator_index != -1: return base64_str[separator_index + 1:] return base64_str class BusController(Controller): def _sync_to_other_devices(self, employee, source_device, photo=None): """ 将员工人脸信息同步到同公司的其他设备 利用 attendance.iot.user 的 create / action_update_iot_info 自动触发 MQTT 推送 """ if not employee or not source_device: return # 查找同公司的其他设备 company = source_device.company_id or employee.company_id other_devices = request.env(user=SUPERUSER_ID)['attendance.iot'].search([ ('company_id', '=', company.id if company else False), ('id', '!=', source_device.id), ]) if not other_devices: return # 如果 photo 不为空,顺便更新员工头像 if photo: photo = urllib.parse.unquote(photo) if isinstance(photo, str) else photo photo = remove_base64_prefix(photo) employee.with_context(iot_sync=True).write({'avatar_image': photo}) for device in other_devices: existed = request.env(user=SUPERUSER_ID)['attendance.iot.user'].search([ ('employee_id', '=', employee.id), ('attendance_iot_id', '=', device.id), ], limit=1) if existed: # 已有绑定 → 触发 MQTT editUser 推送到该设备 existed.with_context(iot_sync=False).action_update_iot_info() else: # 未绑定 → 创建绑定,create() 内部会自动触发 MQTT addUser request.env(user=SUPERUSER_ID)['attendance.iot.user'].with_context(iot_sync=False).create({ 'employee_id': employee.id, 'attendance_iot_id': device.id, }) @route('/api/v1/addFace', methods=['POST'], type='json', auth='none', csrf=False) def get_model_kaoqin(self): """ 获取用户录入考勤机数据,和系统内用户绑定关联 :return: """ _logger.info("----------------/api/v1/addFace---------接口收到响应-------------------------------") data = request.httprequest.json user_name = data.get('name') name = data.get('sn') attendance_iot_id = request.env(user=SUPERUSER_ID)["attendance.iot"].search([('name', '=', name)]) if not attendance_iot_id: request.env(user=SUPERUSER_ID)["attendance.iot"].create({'name': name, }) employee = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', user_name)]) # 判断员工是否绑定多台设备 if employee and attendance_iot_id not in employee.attendance_iot_ids.mapped('attendance_iot_id'): employee.write({ 'image_1920': data.get('photo'), 'attendance_iot_ids': [(0, 0, { 'employee_id': employee.id, 'attendance_address': '', 'attendance_iot_id': attendance_iot_id.id })] }) # 同步到同公司的其他设备 self._sync_to_other_devices(employee, attendance_iot_id, data.get('photo')) _logger.info("-------------------------%s创建成功-------------------------------" % (employee.name)) # 更新设备最后触发时间 & 写日志 if attendance_iot_id: attendance_iot_id.touch_last_update() attendance_iot_id._log('人脸注册', f"员工 {employee.name} 注册到设备 {attendance_iot_id.name}") return {"Result": 0, "Msg": "添加成功"} @route('/api/v1/record/face', methods=['POST'], type='json', auth='none', csrf=False) def get_model_identify(self): """ 获取上传识别成功记录 :return: """ _logger.info("----------------/api/v1/record/face---------接口收到响应-------------------------------") try: data = request.httprequest.json except Exception as e: _logger.info(f'抛出异常,异常原因:{e}') return e # _logger.info(f'考勤机发送过来的数据集合{data}-------我把photo去掉因为太长可') user_id = data.get('logs')[0].get('user_id') recog_time = data.get('logs')[0].get('recog_time') if recog_time: dt = datetime.strptime(recog_time, '%Y-%m-%d %H:%M:%S') dt_adjusted = dt - timedelta(hours=8) recog_time_adjusted = dt_adjusted.strftime('%Y-%m-%d %H:%M:%S') else: recog_time_adjusted = recog_time # 识别时间(时间格式yyyy-MM-dd HH:mm:ss) employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('mobile_phone', '=', user_id)]) _logger.info(f'==========考勤机发送过来的用户和时间{user_id}={employee_id.name}=={recog_time}') attendance_iot_id = request.env(user=SUPERUSER_ID)["attendance.iot"].search([('name', '=', data.get('sn'))]) if employee_id: # 防重复:考勤机会重发旧日志,同一员工+同一打卡时间已存在则跳过 dup_count = request.env(user=SUPERUSER_ID)["hr.attendance.detail"].search_count([ ('employee_id', '=', employee_id.id), ('checkin_time', '=', recog_time_adjusted), ('source', '=', 'iot'), ]) if dup_count: _logger.info(f"重复打卡跳过:{user_id}={recog_time}") else: details = request.env(user=SUPERUSER_ID)["hr.attendance.detail"].create({ 'employee_id': employee_id.id, # 员工 'checkin_time': recog_time_adjusted, 'source': 'iot', # 打卡来源 'attendance_iot_id': attendance_iot_id.id, # 打卡设备 }) _logger.info(f"考勤机创建的数据:{details}") _logger.info(f"---考勤机名称-id:{attendance_iot_id.name}-{attendance_iot_id.id}-===okokokokokokokokokokok==----") # 更新设备最后触发时间 & 写日志 if attendance_iot_id: attendance_iot_id.touch_last_update() attendance_iot_id._log('打卡上报', f"员工 {employee_id.name} 在 {recog_time} 打卡") return {"Result": 0, "Msg": "添加成功"} @route('/api/v1/user/inf_photo', methods=['POST'], type='json', auth='none', csrf=False) def inf_photo(self): """ 更新用户信息 :return: """ _logger.info("----------------/api/v1/user/inf_photo---------接口收到响应-------------------------------") data = request.httprequest.json user_data = data['content'][0] user_id = user_data.get('user_id') vl_photo = user_data.get('vl_photo') sn = data.get('sn') # 设备号 # 查找员工 employee = request.env(user=SUPERUSER_ID)['hr.employee'].search([('mobile_phone', '=', user_id)], limit=1) iot_device = request.env(user=SUPERUSER_ID)['attendance.iot'].search([('name', '=', sn)], limit=1) if not iot_device: request.env(user=SUPERUSER_ID)["attendance.iot"].create({'name': sn}) iot_users = request.env(user=SUPERUSER_ID)['attendance.iot.user'].search( [('employee_id', '=', employee.id), ('attendance_iot_id', '=', iot_device.id)]) # 转码,去前缀,更新 vl_photo = urllib.parse.unquote(vl_photo) vl_photo = remove_base64_prefix(vl_photo) employee.write({'avatar_image': vl_photo}) if not iot_users: request.env(user=SUPERUSER_ID)['attendance.iot.user'].create({ 'employee_id': employee.id, 'attendance_iot_id': iot_device.id, }) # 同步到同公司的其他设备 self._sync_to_other_devices(employee, iot_device, vl_photo) # 更新设备最后触发时间 & 写日志 if iot_device: iot_device.touch_last_update() iot_device._log('用户信息更新', f"员工 {employee.name} 照片更新/注册") return {"Result": 0, "Msg": "添加成功"} @route('/api/v1/uploadFaceInfo', methods=['POST'], type='json', auth='none', csrf=False) def upload_face_info(self): """ 同步数据更新 :return: """ _logger.info("----------------/api/v1/uploadFaceInfo---------接口收到响应-------------------------------") data = request.httprequest.json _logger.info(f'/api/v1/uploadFaceInfo: 结果:{data}') sn = data.get('sn') user_list = data.get('data') # 遍历 user_list # 查找员工,判重后,写入 model_employee = request.env['hr.employee'].sudo() model_iot = request.env['attendance.iot'].sudo() model_iot_user = request.env['attendance.iot.user'].sudo() iot_device = model_iot.search([('name', '=', sn)], limit=1) if not iot_device: iot_device = model_iot.create({'name': sn}) _logger.info(f'设备:{iot_device.id}') for u in user_list: _logger.info(f'用户数据:{u}') employee = model_employee.search([('mobile_phone', '=', u.get('user_id'))]) iot_users = model_iot_user.search( [('employee_id', '=', employee.id), ('attendance_iot_id', '=', iot_device.id)]) _logger.info(f'用户:{employee.id}') _logger.info(f'iot_users:{iot_users.id}') if not iot_users: # 创建用户记录 iot_sync = True , 不要再同步回设备 model_iot_user.create({ 'employee_id': employee.id, 'attendance_iot_id': iot_device.id }) # 更新头像1 # 更新 photo = u.get('photo') _logger.info(f'同步的用户头像数据:') _logger.info(photo) if photo: photo = urllib.parse.unquote(photo) photo = remove_base64_prefix(photo) employee.with_context(iot_sync=True).write({'avatar_image': photo}) # 同步到同公司的其他设备(uploadFaceInfo 是设备主动发起的数据同步,同样需要推给其他设备) self._sync_to_other_devices(employee, iot_device, u.get('photo')) # 更新设备最后触发时间 & 写日志 if iot_device: iot_device.touch_last_update() iot_device._log('人脸数据同步', f"设备 {iot_device.name} 上报 {len(user_list)} 条用户数据") return {"Result": 0, "Msg": "更新成功"} @http.route('/phone/attendance/list', type='http', auth="none", csrf=False, cors='*') def phone_attendance_list(self, **kw): data = request.env['attendance.iot'].sudo().search([('company_id.name', '=', '集团本部')]).read(['id', 'name', 'attendance_address', 'company_id', 'remake']) return json.dumps({'data': data, 'code': 200, 'message': ''}) @http.route('/phone/attendance/operation', type='http', auth="none", csrf=False, cors='*') def phone_attendance_operation(self, **kw): equipment_id = kw.get('equipment_id') equipment = request.env['attendance.iot'].sudo().browse(int(equipment_id)) action = kw.get('action') if action == 'open': equipment.action_send_open_door_sigle() else: equipment.action_send_close_door_sigle() return json.dumps({'data': '', 'code': 200, 'message': ''})