deploy: 后端模块+前端网站更新,统一线上库名为 game
- 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 一键部署脚本
This commit is contained in:
parent
9e01534de9
commit
c6ccc1d50b
@ -203,8 +203,10 @@ class GameApiController(http.Controller):
|
|||||||
password = data.get('password') or ''
|
password = data.get('password') or ''
|
||||||
if not login or not password:
|
if not login or not password:
|
||||||
return _json({'ok': False, 'error': '账号和密码必填'})
|
return _json({'ok': False, 'error': '账号和密码必填'})
|
||||||
if len(password) < 6:
|
if len(password) < 8:
|
||||||
return _json({'ok': False, 'error': '密码至少 6 位'})
|
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()
|
User = request.env['res.users'].sudo()
|
||||||
if User.search([('login', '=', login)]):
|
if User.search([('login', '=', login)]):
|
||||||
return _json({'ok': False, 'error': '该账号已存在'})
|
return _json({'ok': False, 'error': '该账号已存在'})
|
||||||
|
|||||||
34
addons/game_base/models/page.py
Normal file
34
addons/game_base/models/page.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class GamePage(models.Model):
|
||||||
|
"""页面板块:官网单页(特色 / 背景 / 致玩家等)的容器。"""
|
||||||
|
_name = 'game.page'
|
||||||
|
_description = '页面板块'
|
||||||
|
_order = 'sequence asc'
|
||||||
|
_rec_name = 'title'
|
||||||
|
|
||||||
|
key = fields.Char(string='标识', required=True, index=True,
|
||||||
|
help='前端按 key 取板块,如 features / background / letter')
|
||||||
|
title = fields.Char(string='标题', required=True)
|
||||||
|
subtitle = fields.Char(string='副标题')
|
||||||
|
sequence = fields.Integer(string='排序', default=10)
|
||||||
|
body_html = fields.Html(string='正文', sanitize=True)
|
||||||
|
active = fields.Boolean(string='生效', default=True)
|
||||||
|
item_ids = fields.One2many('game.page.item', 'page_id', string='条目')
|
||||||
|
|
||||||
|
|
||||||
|
class GamePageItem(models.Model):
|
||||||
|
"""页面板块下的条目(卡片)。"""
|
||||||
|
_name = 'game.page.item'
|
||||||
|
_description = '页面条目'
|
||||||
|
_order = 'sequence asc'
|
||||||
|
_rec_name = 'title'
|
||||||
|
|
||||||
|
page_id = fields.Many2one('game.page', string='所属板块',
|
||||||
|
required=True, ondelete='cascade', index=True)
|
||||||
|
sequence = fields.Integer(string='排序', default=10)
|
||||||
|
icon = fields.Char(string='图标', help='emoji 或符号,如 🌐 / ✦')
|
||||||
|
title = fields.Char(string='标题', required=True)
|
||||||
|
desc = fields.Text(string='描述')
|
||||||
@ -31,11 +31,11 @@ class YtWorldTile(models.Model):
|
|||||||
for t in self:
|
for t in self:
|
||||||
t.coord = "%s,%s" % (t.q, t.r)
|
t.coord = "%s,%s" % (t.q, t.r)
|
||||||
|
|
||||||
@api.model
|
@api.model_create_multi
|
||||||
def create(self, vals):
|
def create(self, vals_list):
|
||||||
rec = super(YtWorldTile, self).create(vals)
|
records = super(YtWorldTile, self).create(vals_list)
|
||||||
rec._ensure_micro()
|
records._ensure_micro()
|
||||||
return rec
|
return records
|
||||||
|
|
||||||
def _ensure_micro(self):
|
def _ensure_micro(self):
|
||||||
"""新建地块时同步建 1:1 微观世界占位。"""
|
"""新建地块时同步建 1:1 微观世界占位。"""
|
||||||
@ -44,7 +44,6 @@ class YtWorldTile(models.Model):
|
|||||||
m = self.env['yt.world.tile.micro'].create({'tile_id': t.id})
|
m = self.env['yt.world.tile.micro'].create({'tile_id': t.id})
|
||||||
t.micro_world_id = m
|
t.micro_world_id = m
|
||||||
|
|
||||||
@api.model
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
# 任何编辑都刷新 updated_at
|
# 任何编辑都刷新 updated_at
|
||||||
if 'updated_at' not in vals:
|
if 'updated_at' not in vals:
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_yt_world,yt.world,model_yt_world,base.group_user,1,1,1,1
|
access_yt_world_user,yt.world user,model_yt_world,base.group_user,1,1,1,1
|
||||||
access_yt_world_tile,yt.world.tile,model_yt_world_tile,base.group_user,1,1,1,1
|
access_yt_world_public,yt.world public,model_yt_world,base.group_public,1,1,1,1
|
||||||
access_yt_world_tile_micro,yt.world.tile.micro,model_yt_world_tile_micro,base.group_user,1,1,1,1
|
access_yt_world_portal,yt.world portal,model_yt_world,base.group_portal,1,1,1,1
|
||||||
access_yt_resource,yt.resource,model_yt_resource,base.group_user,1,1,1,1
|
access_yt_world_tile_user,yt.world.tile user,model_yt_world_tile,base.group_user,1,1,1,1
|
||||||
|
access_yt_world_tile_public,yt.world.tile public,model_yt_world_tile,base.group_public,1,1,1,1
|
||||||
|
access_yt_world_tile_portal,yt.world.tile portal,model_yt_world_tile,base.group_portal,1,1,1,1
|
||||||
|
access_yt_world_tile_micro_user,yt.world.tile.micro user,model_yt_world_tile_micro,base.group_user,1,1,1,1
|
||||||
|
access_yt_world_tile_micro_public,yt.world.tile.micro public,model_yt_world_tile_micro,base.group_public,1,1,1,1
|
||||||
|
access_yt_world_tile_micro_portal,yt.world.tile.micro portal,model_yt_world_tile_micro,base.group_portal,1,1,1,1
|
||||||
|
access_yt_resource_user,yt.resource user,model_yt_resource,base.group_user,1,1,1,1
|
||||||
|
access_yt_resource_public,yt.resource public,model_yt_resource,base.group_public,1,1,1,1
|
||||||
|
access_yt_resource_portal,yt.resource portal,model_yt_resource,base.group_portal,1,1,1,1
|
||||||
|
|||||||
|
@ -1,13 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<!-- ============ 顶层菜单(带品牌 logo,直接打开玩家世界列表) ============ -->
|
|
||||||
<menuitem id="menu_yt_world_root"
|
|
||||||
name="宇森世界"
|
|
||||||
sequence="20"
|
|
||||||
action="action_yt_world"
|
|
||||||
web_icon="yt_world,static/description/icon.png"/>
|
|
||||||
|
|
||||||
<!-- ============ 玩家世界 ============ -->
|
<!-- ============ 玩家世界 ============ -->
|
||||||
<record id="action_yt_world" model="ir.actions.act_window">
|
<record id="action_yt_world" model="ir.actions.act_window">
|
||||||
<field name="name">玩家世界</field>
|
<field name="name">玩家世界</field>
|
||||||
@ -62,9 +55,6 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ============ 地表地块 ============ -->
|
<!-- ============ 地表地块 ============ -->
|
||||||
<menuitem id="menu_yt_world_tile" name="地表地块"
|
|
||||||
parent="menu_yt_world_root" action="action_yt_world_tile" sequence="10"/>
|
|
||||||
|
|
||||||
<record id="action_yt_world_tile" model="ir.actions.act_window">
|
<record id="action_yt_world_tile" model="ir.actions.act_window">
|
||||||
<field name="name">地表地块</field>
|
<field name="name">地表地块</field>
|
||||||
<field name="res_model">yt.world.tile</field>
|
<field name="res_model">yt.world.tile</field>
|
||||||
@ -108,4 +98,92 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 微观世界(每个地块 1:1) ============ -->
|
||||||
|
<record id="action_yt_world_tile_micro" model="ir.actions.act_window">
|
||||||
|
<field name="name">微观世界</field>
|
||||||
|
<field name="res_model">yt.world.tile.micro</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_tile_micro_list" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.tile.micro.list</field>
|
||||||
|
<field name="model">yt.world.tile.micro</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="tile_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_world_tile_micro_form" model="ir.ui.view">
|
||||||
|
<field name="name">yt.world.tile.micro.form</field>
|
||||||
|
<field name="model">yt.world.tile.micro</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="tile_id"/>
|
||||||
|
<field name="name"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 资源(框架) ============ -->
|
||||||
|
<record id="action_yt_resource" model="ir.actions.act_window">
|
||||||
|
<field name="name">资源</field>
|
||||||
|
<field name="res_model">yt.resource</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_resource_list" model="ir.ui.view">
|
||||||
|
<field name="name">yt.resource.list</field>
|
||||||
|
<field name="model">yt.resource</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="tile_id"/>
|
||||||
|
<field name="micro_id"/>
|
||||||
|
<field name="type"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="capacity"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_yt_resource_form" model="ir.ui.view">
|
||||||
|
<field name="name">yt.resource.form</field>
|
||||||
|
<field name="model">yt.resource</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="tile_id"/>
|
||||||
|
<field name="micro_id"/>
|
||||||
|
<field name="type"/>
|
||||||
|
<field name="amount"/>
|
||||||
|
<field name="capacity"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- ============ 菜单(全部 record 之后,确保 action 已存在) ============ -->
|
||||||
|
<menuitem id="menu_yt_world_root"
|
||||||
|
name="宇森世界"
|
||||||
|
sequence="20"
|
||||||
|
action="action_yt_world"
|
||||||
|
web_icon="yt_world,static/description/icon.png"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_yt_world_tile" name="地表地块"
|
||||||
|
parent="menu_yt_world_root" action="action_yt_world_tile" sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_yt_world_tile_micro" name="微观世界"
|
||||||
|
parent="menu_yt_world_root" action="action_yt_world_tile_micro" sequence="20"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_yt_resource" name="资源"
|
||||||
|
parent="menu_yt_world_root" action="action_yt_resource" sequence="30"/>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
40
deploy_server.sh
Normal file
40
deploy_server.sh
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 宇森游戏 · 线上服务器一键更新(请在 81.70.146.51 上执行)
|
||||||
|
# 用法:sudo bash deploy_server.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ===== 按实际情况修改这几个变量 =====
|
||||||
|
REPO_DIR="/opt/yuthon/game" # git 仓库目录(含 world.html 与 addons/)
|
||||||
|
ODOO_CONF="/etc/odoo/odoo.conf" # odoo.conf 路径
|
||||||
|
ODOO_DB="game" # 线上数据库名(务必与前端 ?db= 一致)
|
||||||
|
ODOO_SERVICE="odoo" # systemctl 服务名
|
||||||
|
ODOO_BIN="/usr/bin/odoo" # odoo-bin 路径(或 which odoo)
|
||||||
|
# ===================================
|
||||||
|
|
||||||
|
echo "== [1/5] 拉取最新代码 =="
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
git pull --ff-only origin main
|
||||||
|
|
||||||
|
echo "== [2/5] 钉死 odoo.conf 数据库 =="
|
||||||
|
if grep -q "^[[:space:]]*db_name" "$ODOO_CONF"; then
|
||||||
|
sed -i "s/^[[:space:]]*db_name.*/db_name = $ODOO_DB/" "$ODOO_CONF"
|
||||||
|
else
|
||||||
|
echo "db_name = $ODOO_DB" >> "$ODOO_CONF"
|
||||||
|
fi
|
||||||
|
if ! grep -q "^[[:space:]]*dbfilter" "$ODOO_CONF"; then
|
||||||
|
echo "dbfilter = ^$ODOO_DB\$" >> "$ODOO_CONF"
|
||||||
|
fi
|
||||||
|
echo "当前 db_name=$(grep '^db_name' "$ODOO_CONF")"
|
||||||
|
|
||||||
|
echo "== [3/5] 升级模块(数据库 $ODOO_DB)=="
|
||||||
|
"$ODOO_BIN" -c "$ODOO_CONF" -d "$ODOO_DB" -u game_base,yt_world --stop-after-init
|
||||||
|
# 若模块从未安装过,把上行的 -u 改成 -i 执行一次即可
|
||||||
|
|
||||||
|
echo "== [4/5] 重启 Odoo =="
|
||||||
|
systemctl restart "$ODOO_SERVICE"
|
||||||
|
sleep 3
|
||||||
|
systemctl is-active "$ODOO_SERVICE" || { echo "Odoo 未起来,检查: journalctl -u $ODOO_SERVICE -n 50"; exit 1; }
|
||||||
|
|
||||||
|
echo "== [5/5] 冒烟验证 =="
|
||||||
|
curl -s -o /dev/null -w "codex 接口 HTTP %{http_code}\n" "https://game.pengyuthon.cn/game/api/codex?db=game"
|
||||||
|
echo "完成。前端为静态文件,nginx 指向 $REPO_DIR 即可,无需构建。"
|
||||||
@ -81,7 +81,7 @@
|
|||||||
<form id="loginForm" autocomplete="off">
|
<form id="loginForm" autocomplete="off">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>账号</label>
|
<label>账号</label>
|
||||||
<input id="lLogin" type="text" placeholder="Odoo 用户名" />
|
<input id="lLogin" type="text" placeholder="用户名" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>密码</label>
|
<label>密码</label>
|
||||||
|
|||||||
38
world.html
38
world.html
@ -3323,22 +3323,42 @@ init();
|
|||||||
<button class="nc-se" data-cmd="se">↘</button>
|
<button class="nc-se" data-cmd="se">↘</button>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// 右下角导航控件事件
|
// 右下角导航控件:方向按钮 + 键盘方向键,统一平移相机
|
||||||
const PAN_STEP = 120; // [PLACEHOLDER] 每按一步平移像素量
|
const PAN_STEP = 120; // [PLACEHOLDER] 每按一步平移像素量
|
||||||
|
function panCamera(dx, dy) {
|
||||||
|
if (microViewOpen) return; // 微观视图打开时不平移主世界
|
||||||
|
camera.x += dx;
|
||||||
|
camera.y += dy;
|
||||||
|
}
|
||||||
document.getElementById('navControls').addEventListener('click', e => {
|
document.getElementById('navControls').addEventListener('click', e => {
|
||||||
const btn = e.target.closest('button');
|
const btn = e.target.closest('button');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
switch (btn.dataset.cmd) {
|
switch (btn.dataset.cmd) {
|
||||||
case 'up': camera.y -= PAN_STEP; break;
|
case 'up': panCamera(0, -PAN_STEP); break;
|
||||||
case 'down': camera.y += PAN_STEP; break;
|
case 'down': panCamera(0, PAN_STEP); break;
|
||||||
case 'left': camera.x -= PAN_STEP; break;
|
case 'left': panCamera(-PAN_STEP, 0); break;
|
||||||
case 'right': camera.x += PAN_STEP; break;
|
case 'right': panCamera( PAN_STEP, 0); break;
|
||||||
case 'nw': camera.x -= PAN_STEP; camera.y -= PAN_STEP; break;
|
case 'nw': panCamera(-PAN_STEP, -PAN_STEP); break;
|
||||||
case 'ne': camera.x += PAN_STEP; camera.y -= PAN_STEP; break;
|
case 'ne': panCamera( PAN_STEP, -PAN_STEP); break;
|
||||||
case 'sw': camera.x -= PAN_STEP; camera.y += PAN_STEP; break;
|
case 'sw': panCamera(-PAN_STEP, PAN_STEP); break;
|
||||||
case 'se': camera.x += PAN_STEP; camera.y += PAN_STEP; break;
|
case 'se': panCamera( PAN_STEP, PAN_STEP); break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 键盘方向键 = 等效于方向按钮(按住时系统自动重复 keydown → 连续平移)
|
||||||
|
const KEY_PAN = {
|
||||||
|
ArrowUp: [0, -PAN_STEP],
|
||||||
|
ArrowDown: [0, PAN_STEP],
|
||||||
|
ArrowLeft: [-PAN_STEP, 0],
|
||||||
|
ArrowRight: [ PAN_STEP, 0],
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', e => {
|
||||||
|
if (!(e.key in KEY_PAN)) return;
|
||||||
|
if (microViewOpen) return; // 微观视图不平移主世界
|
||||||
|
const t = e.target;
|
||||||
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
||||||
|
e.preventDefault(); // 阻止方向键滚动页面
|
||||||
|
panCamera(KEY_PAN[e.key][0], KEY_PAN[e.key][1]);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="assets/game-api.js"></script>
|
<script src="assets/game-api.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user