# -*- coding: utf-8 -*- import json from odoo import http, fields from odoo.http import request, Response def _json(data): """返回 UTF-8 JSON 响应,并附加本地跨域调试头。""" origin = request.httprequest.headers.get('Origin', '') resp = Response( json.dumps(data, ensure_ascii=False), content_type='application/json; charset=utf-8', ) # 允许 127.0.0.1:8888 本地前端跨域调试(生产环境走 nginx 同域,无需此头) if origin.startswith('http://127.0.0.1:') or origin.startswith('http://localhost:'): resp.headers['Access-Control-Allow-Origin'] = origin resp.headers['Access-Control-Allow-Credentials'] = 'true' resp.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' resp.headers['Access-Control-Allow-Headers'] = 'Content-Type' return resp def _post_body(): try: # Odoo 官方解析 JSON 请求体(正确处理 UTF-8),优先使用。 return request.get_json_data() except Exception: try: # 兜底:显式按 UTF-8 解码原始字节,避免 Werkzeug 在 application/json # 无 charset 时按 latin1 解码中文导致乱码。 raw = request.httprequest.get_data(cache=True) or b'{}' if isinstance(raw, str): raw = raw.encode('utf-8') return json.loads(raw.decode('utf-8')) except Exception: return {} class GameApiController(http.Controller): # ============ 登录 / 注册 / 会话(与 Odoo res.users 打通) ============ # 说明:官网只读数据接口(codex/rules/pages/threads/announcements/story 等) # 已迁移至 yusen_website/controllers/site_api.py。本控制器仅保留玩家会话相关接口。 @http.route('/game/api/login', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False) def login(self, **kw): data = _post_body() db = data.get('db') or request.session.db or 'game' login = (data.get('login') or '').strip() password = data.get('password') or '' if not login or not password: return _json({'ok': False, 'error': '账号和密码必填'}) try: auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password}) uid = auth_info['uid'] except Exception: return _json({'ok': False, 'error': '账号或密码错误'}) if uid: user = request.env['res.users'].sudo().browse(uid) return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login}) return _json({'ok': False, 'error': '账号或密码错误'}) @http.route('/game/api/register', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False) def register(self, **kw): data = _post_body() name = (data.get('name') or '').strip() login = (data.get('login') or '').strip() email = (data.get('email') or '').strip() password = data.get('password') or '' if not login or not password: return _json({'ok': False, 'error': '账号和密码必填'}) if len(password) < 8: return _json({'ok': False, 'error': '密码至少 8 位'}) if not any(c.isalpha() for c in password) or not any(c.isdigit() for c in password): return _json({'ok': False, 'error': '密码必须同时包含字母和数字'}) User = request.env['res.users'].sudo() if User.search([('login', '=', login)]): return _json({'ok': False, 'error': '该账号已存在'}) if name and User.search([('name', '=', name)]): return _json({'ok': False, 'error': '该昵称已被使用'}) try: portal = request.env.ref('base.group_portal') user = User.create({ 'name': name or login, 'login': login, 'email': email or False, 'password': password, 'groups_id': [(6, 0, [portal.id])], }) request.env.cr.commit() # 让新用户立即对其他游标可见,便于随后自动登录 # === 自动创建 hr.employee 并初始化游戏数据 === Emp = request.env['hr.employee'].sudo() emp = Emp.create({ 'name': name or login, 'user_id': user.id, 'work_email': email or False, 'game_title': '萌新', 'game_level': 1, 'game_exp': 0, 'game_exp_max': 100, 'game_coins': 0, 'game_energy': 0, 'game_wood': 0, 'game_stone': 0, 'game_reg_date': fields.Date.today(), }) # 生成游戏 UID(格式 10086 + 4位员工ID) emp.game_uid = '10086%04d' % emp.id # 给新玩家解锁第一个成就"开天辟地" tmpl = request.env['game.achievement.template'].sudo().search( [('code', '=', 'first_land')], limit=1) if tmpl: request.env['game.player.achievement'].sudo().create({ 'employee_id': emp.id, 'template_id': tmpl.id, 'progress': tmpl.target, 'unlocked': True, 'unlock_date': fields.Datetime.now(), }) # 给新玩家发一封欢迎邮件 request.env['game.player.mail'].sudo().create({ 'employee_id': emp.id, 'category': 'system', 'subject': '欢迎来到宇森', 'sender': '系统', 'body': '
欢迎来到宇森!请查收新手礼包与世界探索指南。
', }) request.env.cr.commit() db = request.session.db or 'game' auth_info = request.session.authenticate(db, {'type': 'password', 'login': login, 'password': password}) uid = auth_info['uid'] if uid: return _json({'ok': True, 'uid': uid, 'name': user.name, 'login': user.login, 'emp_id': emp.id, 'game_uid': emp.game_uid}) return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login, 'emp_id': emp.id, 'game_uid': emp.game_uid}) except Exception as e: return _json({'ok': False, 'error': '注册失败:%s' % str(e)}) @http.route('/game/api/me', type='http', auth='public', methods=['GET'], csrf=False) def me(self, **kw): uid = request.session.uid if uid: user = request.env['res.users'].sudo().browse(uid) if user.exists(): data = {'uid': uid, 'name': user.name, 'login': user.login, 'email': user.email or ''} emp = request.env['hr.employee'].sudo().search( [('user_id', '=', uid)], limit=1) if emp: data.update(emp.export_game_data()) return _json(data) return _json({'uid': None}) @http.route('/game/api/achievements', type='http', auth='public', methods=['GET'], csrf=False) def achievements(self, **kw): """返回当前登录玩家的成就列表(含模板+进度)。""" uid = request.session.uid if not uid: return _json({'error': '未登录'}) emp = request.env['hr.employee'].sudo().search( [('user_id', '=', uid)], limit=1) if not emp: return _json({'error': '玩家不存在', 'items': [], 'total_points': 0}) # 取所有启用的成就模板 tmpl_model = request.env['game.achievement.template'].sudo() tier_labels = dict(tmpl_model._fields['tier'].selection) templates = tmpl_model.search_read( [('active', '=', True)], ['id', 'name', 'icon', 'tier', 'desc', 'points', 'target', 'sequence'], order='sequence, id') # 取该玩家已解锁的成就 player_recs = request.env['game.player.achievement'].sudo().search_read( [('employee_id', '=', emp.id)], ['template_id', 'progress', 'unlocked', 'unlock_date']) progress_map = { r['template_id'][0]: r for r in player_recs } items = [] total_points = 0 unlocked_count = 0 for t in templates: pr = progress_map.get(t['id']) unlocked = pr and pr['unlocked'] progress = pr['progress'] if pr else 0 if unlocked: total_points += t['points'] unlocked_count += 1 items.append({ 'id': t['id'], 'name': t['name'], 'icon': t.get('icon') or '', 'tier': t['tier'], 'tier_label': tier_labels.get(t['tier'], t['tier']), 'desc': t.get('desc') or '', 'points': t['points'], 'target': t['target'], 'progress': progress, 'unlocked': bool(unlocked), 'unlock_date': str(pr['unlock_date']) if pr and pr['unlock_date'] else None, }) return _json({ 'items': items, 'total_points': total_points, 'unlocked_count': unlocked_count, 'total_count': len(items), }) @http.route('/game/api/mails', type='http', auth='public', methods=['GET'], csrf=False) def mails(self, **kw): """返回当前登录玩家的邮件列表。""" uid = request.session.uid if not uid: return _json({'error': '未登录', 'items': []}) emp = request.env['hr.employee'].sudo().search( [('user_id', '=', uid)], limit=1) if not emp: return _json({'error': '玩家不存在', 'items': []}) cat_labels = dict( request.env['game.player.mail'].sudo()._fields['category'].selection) recs = request.env['game.player.mail'].sudo().search_read( [('employee_id', '=', emp.id)], ['id', 'category', 'subject', 'body', 'date', 'is_read', 'is_pinned', 'sender'], order='is_pinned desc, date desc') items = [{ 'id': r['id'], 'category': r['category'], 'category_label': cat_labels.get(r['category'], r['category']), 'subject': r['subject'], 'body': r.get('body') or '', 'date': str(r['date']) if r['date'] else '', 'is_read': r['is_read'], 'is_pinned': r['is_pinned'], 'sender': r.get('sender') or '', } for r in recs] unread = len([r for r in items if not r['is_read']]) return _json({'items': items, 'unread': unread}) @http.route('/game/api/mail/read', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False) def mail_read(self, **kw): """标记邮件为已读。""" uid = request.session.uid if not uid: return _json({'ok': False, 'error': '未登录'}) data = _post_body() mail_id = data.get('mail_id') if not mail_id: return _json({'ok': False, 'error': '缺少 mail_id'}) emp = request.env['hr.employee'].sudo().search( [('user_id', '=', uid)], limit=1) if not emp: return _json({'ok': False, 'error': '玩家不存在'}) mail = request.env['game.player.mail'].sudo().browse(mail_id) if not mail.exists() or mail.employee_id != emp: return _json({'ok': False, 'error': '邮件不存在或无权操作'}) mail.write({'is_read': True}) return _json({'ok': True}) @http.route('/game/api/logout', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False) def logout(self, **kw): request.session.logout() return _json({'ok': True})