- game_base: 新增 game.page/page.item 模型(page.py),密码策略加严(>=8位且含字母+数字) - yt_world: create 改 create_multi;权限扩至 public/portal/user;新增微观世界/资源视图与菜单 - 前端: website/auth/index 等 ?db=yt_game 统一改为 ?db=game(线上库名) - 新增 deploy_server.sh 一键部署脚本
246 lines
10 KiB
Python
246 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
from odoo import http
|
|
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):
|
|
"""规则 / 等级制度:按类别(type)分组。"""
|
|
model = request.env['game.rule'].sudo()
|
|
type_labels = dict(model._fields['type'].selection)
|
|
recs = model.search_read([], ['id', 'type', 'level', 'name', 'desc'])
|
|
by_type = {}
|
|
for r in recs:
|
|
by_type.setdefault(r['type'], []).append(r)
|
|
groups = []
|
|
for t in by_type:
|
|
groups.append({
|
|
'key': t,
|
|
'label': type_labels.get(t, t),
|
|
'items': [{
|
|
'id': r['id'],
|
|
'level': r['level'],
|
|
'name': r['name'],
|
|
'desc': r['desc'] or '',
|
|
} for r in by_type[t]],
|
|
})
|
|
return _json({'groups': groups})
|
|
|
|
@http.route('/game/api/currencies', type='http', auth='public', methods=['GET'], csrf=False)
|
|
def currencies(self, **kw):
|
|
"""货币等级:按 level 排序返回。"""
|
|
recs = request.env['game.currency'].sudo().search_read(
|
|
[], ['id', 'level', 'name', 'desc', 'rate'], order='level asc'
|
|
)
|
|
return _json({
|
|
'items': [{
|
|
'id': r['id'],
|
|
'level': r['level'],
|
|
'name': r['name'],
|
|
'desc': r['desc'] or '',
|
|
'rate': r['rate'] or 1,
|
|
} for r in recs]
|
|
})
|
|
|
|
@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)
|
|
|
|
# ============ 登录 / 注册 / 会话(与 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': '该账号已存在'})
|
|
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() # 让新用户立即对其他游标可见,便于随后自动登录
|
|
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})
|
|
return _json({'ok': True, 'uid': user.id, 'name': user.name, 'login': user.login})
|
|
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():
|
|
return _json({'uid': uid, 'name': user.name, 'login': user.login})
|
|
return _json({'uid': None})
|
|
|
|
@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})
|