Files
game/addons/game_base/controllers/game_api.py
T
李鹏宇 73d764938d feat: 前端 UI 精修 + 后端视图调整 + 资源预置
前端:
- world.html 游戏内聊天组件重构:表情选择器改固定方格+自适应列+去滚动条;
  输入框清空占位文字+隐藏滚动条;聊天面板整体高度缩短约 1/3;
  顶部用户区改为纯文字并点开个人信息弹窗(头像/头像框/性别/说明/称号/注册)
- chat.html 聊天输入框压扁(46→34px)
- codex.html 新增神话品质、天赋/技能书/书籍分类与预置数据
- rules.html 规则文档更新

后端(Odoo):
- game_base 附件改为看板视图(图片/名称/备注/大小)
- yt_world 玩家菜单直接指向 hr.employee
- game_base 员工表单隐藏原生联系人/组织架构页签
- chat 模型/接口/视图与规则数据扩充

资源:生成 4x4 资源精灵表(树木/石材/芦苇/宝箱)
2026-08-14 14:27:15 +08:00

270 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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': '<p>欢迎来到宇森!请查收新手礼包与世界探索指南。</p>',
})
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})