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:
李鹏宇 2026-07-24 10:00:51 +08:00
parent 9e01534de9
commit c6ccc1d50b
8 changed files with 213 additions and 32 deletions

View File

@ -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': '该账号已存在'})

View 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='描述')

View File

@ -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:

View File

@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_yt_world access_yt_world_user yt.world yt.world user model_yt_world base.group_user 1 1 1 1
3 access_yt_world_tile access_yt_world_public yt.world.tile yt.world public model_yt_world_tile model_yt_world base.group_user base.group_public 1 1 1 1
4 access_yt_world_tile_micro access_yt_world_portal yt.world.tile.micro yt.world portal model_yt_world_tile_micro model_yt_world base.group_user base.group_portal 1 1 1 1
5 access_yt_resource access_yt_world_tile_user yt.resource yt.world.tile user model_yt_resource model_yt_world_tile base.group_user 1 1 1 1
6 access_yt_world_tile_public yt.world.tile public model_yt_world_tile base.group_public 1 1 1 1
7 access_yt_world_tile_portal yt.world.tile portal model_yt_world_tile base.group_portal 1 1 1 1
8 access_yt_world_tile_micro_user yt.world.tile.micro user model_yt_world_tile_micro base.group_user 1 1 1 1
9 access_yt_world_tile_micro_public yt.world.tile.micro public model_yt_world_tile_micro base.group_public 1 1 1 1
10 access_yt_world_tile_micro_portal yt.world.tile.micro portal model_yt_world_tile_micro base.group_portal 1 1 1 1
11 access_yt_resource_user yt.resource user model_yt_resource base.group_user 1 1 1 1
12 access_yt_resource_public yt.resource public model_yt_resource base.group_public 1 1 1 1
13 access_yt_resource_portal yt.resource portal model_yt_resource base.group_portal 1 1 1 1

View File

@ -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
View 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 即可,无需构建。"

View File

@ -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>

View File

@ -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>