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 ''
|
||||
if not login or not password:
|
||||
return _json({'ok': False, 'error': '账号和密码必填'})
|
||||
if len(password) < 6:
|
||||
return _json({'ok': False, 'error': '密码至少 6 位'})
|
||||
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': '该账号已存在'})
|
||||
|
||||
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:
|
||||
t.coord = "%s,%s" % (t.q, t.r)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
rec = super(YtWorldTile, self).create(vals)
|
||||
rec._ensure_micro()
|
||||
return rec
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super(YtWorldTile, self).create(vals_list)
|
||||
records._ensure_micro()
|
||||
return records
|
||||
|
||||
def _ensure_micro(self):
|
||||
"""新建地块时同步建 1:1 微观世界占位。"""
|
||||
@ -44,7 +44,6 @@ class YtWorldTile(models.Model):
|
||||
m = self.env['yt.world.tile.micro'].create({'tile_id': t.id})
|
||||
t.micro_world_id = m
|
||||
|
||||
@api.model
|
||||
def write(self, vals):
|
||||
# 任何编辑都刷新 updated_at
|
||||
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
|
||||
access_yt_world,yt.world,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_tile_micro,yt.world.tile.micro,model_yt_world_tile_micro,base.group_user,1,1,1,1
|
||||
access_yt_resource,yt.resource,model_yt_resource,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_public,yt.world public,model_yt_world,base.group_public,1,1,1,1
|
||||
access_yt_world_portal,yt.world portal,model_yt_world,base.group_portal,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"?>
|
||||
<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">
|
||||
<field name="name">玩家世界</field>
|
||||
@ -62,9 +55,6 @@
|
||||
</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">
|
||||
<field name="name">地表地块</field>
|
||||
<field name="res_model">yt.world.tile</field>
|
||||
@ -108,4 +98,92 @@
|
||||
</field>
|
||||
</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>
|
||||
|
||||
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">
|
||||
<div class="field">
|
||||
<label>账号</label>
|
||||
<input id="lLogin" type="text" placeholder="Odoo 用户名" />
|
||||
<input id="lLogin" type="text" placeholder="用户名" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>密码</label>
|
||||
|
||||
38
world.html
38
world.html
@ -3323,22 +3323,42 @@ init();
|
||||
<button class="nc-se" data-cmd="se">↘</button>
|
||||
</div>
|
||||
<script>
|
||||
// 右下角导航控件事件
|
||||
// 右下角导航控件:方向按钮 + 键盘方向键,统一平移相机
|
||||
const PAN_STEP = 120; // [PLACEHOLDER] 每按一步平移像素量
|
||||
function panCamera(dx, dy) {
|
||||
if (microViewOpen) return; // 微观视图打开时不平移主世界
|
||||
camera.x += dx;
|
||||
camera.y += dy;
|
||||
}
|
||||
document.getElementById('navControls').addEventListener('click', e => {
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
switch (btn.dataset.cmd) {
|
||||
case 'up': camera.y -= PAN_STEP; break;
|
||||
case 'down': camera.y += PAN_STEP; break;
|
||||
case 'left': camera.x -= PAN_STEP; break;
|
||||
case 'right': camera.x += PAN_STEP; break;
|
||||
case 'nw': camera.x -= PAN_STEP; camera.y -= PAN_STEP; break;
|
||||
case 'ne': camera.x += PAN_STEP; camera.y -= PAN_STEP; break;
|
||||
case 'sw': camera.x -= PAN_STEP; camera.y += PAN_STEP; break;
|
||||
case 'se': camera.x += PAN_STEP; camera.y += PAN_STEP; break;
|
||||
case 'up': panCamera(0, -PAN_STEP); break;
|
||||
case 'down': panCamera(0, PAN_STEP); break;
|
||||
case 'left': panCamera(-PAN_STEP, 0); break;
|
||||
case 'right': panCamera( PAN_STEP, 0); break;
|
||||
case 'nw': panCamera(-PAN_STEP, -PAN_STEP); break;
|
||||
case 'ne': panCamera( PAN_STEP, -PAN_STEP); break;
|
||||
case 'sw': panCamera(-PAN_STEP, 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 src="assets/game-api.js"></script>
|
||||
<script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user