game/addons/game_base/controllers/game_api.py
2026-07-28 19:06:32 +08:00

489 lines
21 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:
raw = request.httprequest.get_data(as_text=True) or '{}'
return json.loads(raw)
except Exception:
return {}
class GameApiController(http.Controller):
# ============ 公开只读数据接口(前端从 Odoo 取数) ============
@http.route('/game/api/codex', type='http', auth='public', methods=['GET'], csrf=False)
def codex(self, **kw):
"""图鉴:按分类(cat)分组,每组内按子级(sub)分组。"""
model = request.env['game.codex.entry'].sudo()
cat_labels = dict(model._fields['cat'].selection)
recs = model.search_read([], ['id', 'cat', 'sub', 'name', 'tier', 'desc'])
by_cat = {}
for r in recs:
by_cat.setdefault(r['cat'], []).append(r)
groups = []
for cat in by_cat:
subs = {}
for r in by_cat[cat]:
sub = r['sub'] or '其他'
subs.setdefault(sub, []).append({
'id': r['id'],
'name': r['name'],
'tier': r['tier'],
'sub': r['sub'] or '',
'desc': r['desc'] or '',
'image': '/web/image/game.codex.entry/%s/image' % r['id'],
})
groups.append({
'key': cat,
'label': cat_labels.get(cat, cat),
'subs': [{'name': s, 'items': subs[s]} for s in subs],
})
return _json({'groups': groups})
@http.route('/game/api/rules', type='http', auth='public', methods=['GET'], csrf=False)
def rules(self, **kw):
"""规则 / 等级制度 / 货币:按类别分组。"""
Rule = request.env['game.rule'].sudo()
Category = request.env['game.rule.category'].sudo()
cats = Category.search_read(
[('active', '=', True)],
['id', 'key', 'name', 'sequence'],
order='sequence, id'
)
cat_map = {c['id']: c for c in cats}
recs = Rule.search_read(
[],
['id', 'category_id', 'level', 'name', 'desc', 'rate', 'note'],
order='category_id, level'
)
by_cat = {}
for r in recs:
cid = r['category_id'][0] if r['category_id'] else None
if cid not in cat_map:
continue
by_cat.setdefault(cid, []).append(r)
groups = []
for c in cats:
items = by_cat.get(c['id'], [])
if not items:
continue
groups.append({
'key': c['key'],
'label': c['name'],
'items': [{
'id': it['id'],
'level': it['level'],
'name': it['name'],
'desc': it['desc'] or '',
'rate': it.get('rate') or 1,
'note': it.get('note') or '',
} for it in items],
})
return _json({'groups': groups})
@http.route('/game/api/currencies', type='http', auth='public', methods=['GET'], csrf=False)
def currencies(self, **kw):
"""货币等级:兼容旧接口,实际从 game.rule 按类别过滤。"""
Rule = request.env['game.rule'].sudo()
Category = request.env['game.rule.category'].sudo()
currency_keys = ['currency', 'material', 'token', 'honor']
cats = Category.search_read(
[('active', '=', True), ('key', 'in', currency_keys)],
['id', 'key', 'name', 'sequence'],
order='sequence, id'
)
cat_map = {c['id']: c for c in cats}
recs = Rule.search_read(
[('category_id.key', 'in', currency_keys)],
['id', 'category_id', 'level', 'name', 'desc', 'note', 'rate'],
order='category_id, level'
)
by_type = {}
for r in recs:
cid = r['category_id'][0] if r['category_id'] else None
if cid not in cat_map:
continue
by_type.setdefault(cid, []).append(r)
groups = []
for c in cats:
items = by_type.get(c['id'], [])
if not items:
continue
groups.append({
'key': c['key'],
'label': c['name'],
'items': [{
'id': it['id'],
'level': it['level'],
'name': it['name'],
'desc': it['desc'] or '',
'note': it.get('note') or '',
'rate': it.get('rate') or 1,
} for it in items],
})
return _json({'groups': groups})
@http.route('/game/api/pages', type='http', auth='public', methods=['GET'], csrf=False)
def pages(self, **kw):
"""页面板块:返回所有生效板块及其条目。"""
pages = request.env['game.page'].sudo().search_read(
[('active', '=', True)],
['id', 'key', 'title', 'subtitle', 'body_html'],
order='sequence asc'
)
out = []
for p in pages:
items = request.env['game.page.item'].sudo().search_read(
[('page_id', '=', p['id'])],
['id', 'icon', 'title', 'desc'],
order='sequence asc'
)
out.append({
'id': p['id'],
'key': p['key'],
'title': p['title'],
'subtitle': p.get('subtitle') or '',
'body_html': p.get('body_html') or '',
'items': [{
'id': it['id'],
'icon': it.get('icon') or '',
'title': it.get('title') or '',
'desc': it.get('desc') or '',
} for it in items],
})
return _json({'items': out})
@http.route('/game/api/threads', type='http', auth='public', methods=['GET'], csrf=False)
def threads(self, **kw):
"""论坛主题(含回复),按置顶+时间排序。"""
model = request.env['game.forum.thread'].sudo()
cat_labels = dict(model._fields['category'].selection)
recs = model.search_read(
[], ['id', 'name', 'category', 'author_name', 'pinned', 'create_date', 'body_html'])
out = []
for r in recs:
posts = request.env['game.forum.post'].sudo().search_read(
[('thread_id', '=', r['id'])],
['id', 'author_name', 'body_html', 'create_date'])
out.append({
'id': r['id'],
'name': r['name'],
'category': r['category'],
'category_label': cat_labels.get(r['category'], r['category']),
'author_name': r['author_name'] or '',
'pinned': r['pinned'],
'create_date': str(r['create_date']),
'body_html': r['body_html'] or '',
'replies': posts,
})
return _json(out)
@http.route('/game/api/announcements', type='http', auth='public', methods=['GET'], csrf=False)
def announcements(self, **kw):
"""公告(仅生效的)。"""
model = request.env['game.announcement'].sudo()
pri_labels = dict(model._fields['priority'].selection)
recs = model.search_read(
[('active', '=', True)], ['id', 'name', 'date', 'priority', 'body_html'])
out = [{
'id': r['id'],
'name': r['name'],
'date': str(r['date']),
'priority': r['priority'],
'priority_label': pri_labels.get(r['priority'], r['priority']),
'body_html': r['body_html'] or '',
} for r in recs]
return _json(out)
@http.route('/game/api/story', type='http', auth='public', methods=['GET'], csrf=False)
def story(self, **kw):
"""小说目录:返回 卷 -> 章节 结构,供前端左侧目录(TOC)使用。"""
Volume = request.env['game.story.volume'].sudo()
Chapter = request.env['game.story.chapter'].sudo()
vols = Volume.search_read([], ['id', 'name', 'sequence', 'intro'], order='sequence, id')
chapters = Chapter.search_read([], ['id', 'volume_id', 'name', 'sequence'],
order='volume_id, sequence, id')
by_vol = {}
for c in chapters:
vid = c['volume_id'][0] if c['volume_id'] else None
by_vol.setdefault(vid, []).append({
'id': c['id'],
'name': c['name'],
'sequence': c['sequence'],
})
out = []
for v in vols:
out.append({
'id': v['id'],
'name': v['name'],
'sequence': v['sequence'],
'intro': v.get('intro') or '',
'chapters': by_vol.get(v['id'], []),
})
return _json({'volumes': out})
@http.route('/game/api/story/chapter/<int:cid>', type='http', auth='public', methods=['GET'], csrf=False)
def story_chapter(self, cid, **kw):
"""单章正文(点击目录时按需拉取,避免一次性下发整本小说)。"""
rec = request.env['game.story.chapter'].sudo().search_read(
[('id', '=', cid)], ['id', 'volume_id', 'name', 'sequence', 'body_html'])
if not rec:
return _json({'error': '章节不存在'})
r = rec[0]
return _json({
'id': r['id'],
'volume_id': r['volume_id'][0] if r['volume_id'] else None,
'name': r['name'],
'sequence': r['sequence'],
'body_html': r['body_html'] or '',
})
# ============ 登录 / 注册 / 会话(与 Odoo res.users 打通) ============
@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})