454 lines
22 KiB
Python
454 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
import logging
|
|
import re
|
|
from odoo import http
|
|
from odoo.http import request, Response
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _json(data):
|
|
"""返回 UTF-8 JSON 响应(与 game_base 一致),并附带 CORS 头以支持跨域(8080→8069)。"""
|
|
headers = {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
}
|
|
try:
|
|
origin = request.httprequest.headers.get('Origin')
|
|
if origin:
|
|
headers['Access-Control-Allow-Origin'] = origin
|
|
headers['Access-Control-Allow-Credentials'] = 'true'
|
|
else:
|
|
headers['Access-Control-Allow-Origin'] = '*'
|
|
except Exception:
|
|
headers['Access-Control-Allow-Origin'] = '*'
|
|
return Response(json.dumps(data, ensure_ascii=False), headers=headers)
|
|
|
|
|
|
def _post_body():
|
|
try:
|
|
raw = request.httprequest.get_data(as_text=True) or '{}'
|
|
return json.loads(raw)
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
class YtWorldApi(http.Controller):
|
|
"""前端沙盘记忆接口(JSON over HTTP)。auth=user:需已登录 Odoo。
|
|
前端统一带 ?db=game 且 credentials:'same-origin',故路由须为 type='http'
|
|
并以 _json 返回原始 JSON(与 game-api.js 的 fetch().json() 对齐)。"""
|
|
|
|
@http.route('/yt_world/api/world', type='http', auth='user',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def world(self, **kw):
|
|
"""取或建当前用户世界,导出 seed + 地块覆盖。"""
|
|
user = request.env.user
|
|
rec = request.env['yt.world'].sudo().get_or_create_for_user(user)
|
|
return _json(rec.export_state())
|
|
|
|
@http.route('/yt_world/api/tiles/save', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def save_tiles(self, **kw):
|
|
"""增量覆盖保存地块。body: {"tiles": [...]}"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
tiles = body.get('tiles') or []
|
|
world.import_tiles(tiles)
|
|
return _json({'ok': True, 'saved': len(tiles)})
|
|
|
|
@http.route('/yt_world/api/settings', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def save_settings(self, **kw):
|
|
"""保存相机/UI 偏好。body: {"settings": {...}}"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
body = _post_body()
|
|
settings = body.get('settings')
|
|
if world and settings is not None:
|
|
world.settings = settings
|
|
return _json({'ok': True})
|
|
|
|
@http.route('/yt_world/api/world/rename', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def rename_world(self, **kw):
|
|
"""修改当前用户世界名称。body: {"name": "..."}
|
|
校验:trim 后 1-24 字符,禁控制字符,空白正常允许。"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
name = (body.get('name') or '').strip()
|
|
if not name:
|
|
return _json({'ok': False, 'error': 'empty'})
|
|
if len(name) > 24:
|
|
return _json({'ok': False, 'error': 'too_long'})
|
|
if re.search(r'[\x00-\x1f\x7f]', name):
|
|
return _json({'ok': False, 'error': 'invalid_char'})
|
|
old = world.name
|
|
world.name = name
|
|
_logger.info('yt.world rename uid=%s %r -> %r', user.id, old, name)
|
|
return _json({'ok': True, 'name': name})
|
|
|
|
@http.route('/yt_world/api/micro/build/save', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def save_micro_build(self, **kw):
|
|
"""保存某地块微观世界的建造布置。body: {"q":int,"r":int,"items":[{i,j,type}]}
|
|
找不到地块时自动建(保证种子地块也能落库记忆)。"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
q, r, items = body.get('q'), body.get('r'), body.get('items')
|
|
if q is None or r is None or items is None:
|
|
return _json({'ok': False, 'error': 'bad_params'})
|
|
if not isinstance(items, list):
|
|
items = []
|
|
tile = request.env['yt.world.tile'].sudo().search(
|
|
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
|
|
if not tile:
|
|
tile = request.env['yt.world.tile'].sudo().create(
|
|
{'world_id': world.id, 'q': q, 'r': r})
|
|
if not tile.micro_world_id:
|
|
tile._ensure_micro()
|
|
tile.micro_world_id.build_items = items
|
|
return _json({'ok': True, 'count': len(items)})
|
|
|
|
@http.route('/yt_world/api/micro/name', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def rename_micro(self, **kw):
|
|
"""修改某地块微观世界的名称。body: {"q":int,"r":int,"name":"..."}
|
|
找不到地块时自动建(保证种子地块也能落库记忆);与 rename_world 同样的校验。"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
q, r, name = body.get('q'), body.get('r'), (body.get('name') or '').strip()
|
|
if q is None or r is None:
|
|
return _json({'ok': False, 'error': 'bad_params'})
|
|
if not name:
|
|
return _json({'ok': False, 'error': 'empty'})
|
|
if len(name) > 24:
|
|
return _json({'ok': False, 'error': 'too_long'})
|
|
if re.search(r'[\x00-\x1f\x7f]', name):
|
|
return _json({'ok': False, 'error': 'invalid_char'})
|
|
tile = request.env['yt.world.tile'].sudo().search(
|
|
[('world_id', '=', world.id), ('q', '=', q), ('r', '=', r)], limit=1)
|
|
if not tile:
|
|
tile = request.env['yt.world.tile'].sudo().create(
|
|
{'world_id': world.id, 'q': q, 'r': r})
|
|
if not tile.micro_world_id:
|
|
tile._ensure_micro()
|
|
old = tile.micro_world_id.name
|
|
tile.micro_world_id.name = name
|
|
_logger.info('yt.world.tile.micro rename uid=%s (%s,%s) %r -> %r',
|
|
user.id, q, r, old, name)
|
|
return _json({'ok': True, 'name': name, 'q': q, 'r': r})
|
|
|
|
@http.route('/yt_world/api/micro/catalog', type='http', auth='user',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def micro_catalog(self, **kw):
|
|
"""微观世界建造素材目录(前端 MICRO_BUILD_ITEMS 的单一来源)。
|
|
关联当前用户世界:返回「全局模板(world_id空) + 本世界专属」的素材。"""
|
|
user = request.env.user
|
|
world = request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
if world:
|
|
dom = ['|', ('world_id', '=', False), ('world_id', '=', world.id)]
|
|
else:
|
|
dom = [('world_id', '=', False)]
|
|
dom = ['&', ('active', '=', True)] + dom
|
|
items = request.env['yt.world.build.item'].sudo().search(dom, order='sequence, id')
|
|
CAT_LABELS = {
|
|
'build': '建筑', 'facility': '设施', 'scene': '场景',
|
|
'bio': '生物', 'other': '其他', 'road': '道路', 'ground': '地面',
|
|
}
|
|
seen = []
|
|
out = []
|
|
for r in items.read(['code', 'name', 'category', 'icon', 'footprint_w',
|
|
'footprint_h', 'terrains', 'desc', 'flat',
|
|
'stat_level', 'stat_hp', 'stat_yield']):
|
|
if r['category'] not in seen:
|
|
seen.append(r['category'])
|
|
out.append({
|
|
'code': r['code'], 'name': r['name'], 'category': r['category'],
|
|
'icon': r['icon'], 'w': r['footprint_w'], 'h': r['footprint_h'],
|
|
'terrains': r['terrains'] or [],
|
|
'desc': r['desc'] or '',
|
|
'flat': bool(r['flat']),
|
|
'statLevel': r['stat_level'] or 1,
|
|
'statHp': r['stat_hp'] or 100,
|
|
'statYield': r['stat_yield'] or {},
|
|
})
|
|
cats = [{'id': c, 'name': CAT_LABELS.get(c, c)} for c in seen]
|
|
return _json({'cats': cats, 'items': out})
|
|
|
|
# ========== 图鉴 API ==========
|
|
|
|
def _records_json(self, model, fields):
|
|
"""将 model 的全部记录转为 [{field: value}, ...] 格式(Json 字段解析为对象)。"""
|
|
recs = request.env[model].sudo().search([], order='sequence, id')
|
|
out = []
|
|
for r in recs:
|
|
item = {}
|
|
for f in fields:
|
|
val = getattr(r, f)
|
|
if isinstance(val, str):
|
|
try:
|
|
val = json.loads(val)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
item[f] = val
|
|
out.append(item)
|
|
return _json(out)
|
|
|
|
@http.route('/yt_world/api/codex/resources', type='http', auth='public',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def codex_resources(self, **kw):
|
|
return self._records_json('yt.codex.resource',
|
|
['code','name','icon','category','quality','source','uses','recipe','description'])
|
|
|
|
@http.route('/yt_world/api/codex/creatures', type='http', auth='public',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def codex_creatures(self, **kw):
|
|
return self._records_json('yt.codex.creature',
|
|
['code','name','icon','habitat','tameable','diet','products','description'])
|
|
|
|
@http.route('/yt_world/api/codex/monsters', type='http', auth='public',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def codex_monsters(self, **kw):
|
|
return self._records_json('yt.codex.monster',
|
|
['code','name','icon','tier','level_min','level_max','habitat','drops','description'])
|
|
|
|
@http.route('/yt_world/api/codex/races', type='http', auth='public',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def codex_races(self, **kw):
|
|
return self._records_json('yt.codex.race',
|
|
['code','name','icon','layer','traits','description'])
|
|
|
|
# ============================================================
|
|
# 游戏化系统:角色 / 生物 / 订单 / 建筑交互(前端-后端打通)
|
|
# ============================================================
|
|
|
|
# 建筑动作效果表(数据驱动,与前端 BUILD_INTERACT 的 action id 对应)。
|
|
# gain/cost 中 'gold' 表示金币,其余键为资源编码(food/wood/metal/...)。
|
|
# heal: True 时需要将 creature_id 传回,并把该生物生命回满。
|
|
BUILD_EFFECTS = {
|
|
# —— 建筑(3/4 立绘)——
|
|
('cottage', 'employ'): {'msg': '已发布村长任职委托'},
|
|
('cottage', 'recruit'): {'gain': {'gold': 5}, 'msg': '招募村民,人口+1'},
|
|
('cottage', 'upgrade'): {'cost': {'gold': 50}, 'msg': '农舍升级申请已提交'},
|
|
('windmill', 'collect'): {'gain': {'flour': 3}, 'msg': '领取面粉 +3'},
|
|
('windmill', 'produce'): {'cost': {'flour': 2}, 'gain': {'food': 4}, 'msg': '加工粮食 +4'},
|
|
('windmill', 'upgrade'): {'cost': {'gold': 40}, 'msg': '风车升级申请已提交'},
|
|
('blacksmith', 'forge'): {'gain': {'metal': 3}, 'msg': '锻造金属 +3'},
|
|
('blacksmith', 'commission'): {'msg': '已发布装备委托'},
|
|
('blacksmith', 'upgrade'): {'cost': {'gold': 60}, 'msg': '铁匠铺升级申请已提交'},
|
|
('grocery', 'sell'): {'gain': {'gold': 15}, 'msg': '售货收入 +15 金币'},
|
|
('grocery', 'restock'): {'cost': {'gold': 10}, 'gain': {'food': 2, 'wood': 1}, 'msg': '进货:粮+2 木+1'},
|
|
('grocery', 'upgrade'): {'cost': {'gold': 30}, 'msg': '杂货铺升级申请已提交'},
|
|
('temple', 'pray'): {'gain': {'prestige': 2}, 'msg': '祈祷获得声望 +2'},
|
|
('temple', 'donate'): {'cost': {'gold': 20}, 'gain': {'prestige': 5}, 'msg': '捐赠获得声望 +5'},
|
|
('temple', 'upgrade'): {'cost': {'gold': 50}, 'msg': '神庙升级申请已提交'},
|
|
('clinic', 'heal'): {'heal': True, 'msg': '治疗完成,生物生命已回满'},
|
|
('clinic', 'upgrade'): {'cost': {'gold': 40}, 'msg': '医务室升级申请已提交'},
|
|
('restaurant', 'cook'): {'cost': {'food': 2}, 'gain': {'satiety': 3}, 'msg': '烹制料理,饱食 +3'},
|
|
('restaurant', 'upgrade'): {'cost': {'gold': 30}, 'msg': '饭馆升级申请已提交'},
|
|
('pasture', 'breed'): {'gain': {'meat': 2, 'milk': 1}, 'msg': '畜牧产出:肉+2 奶+1'},
|
|
('pasture', 'upgrade'): {'cost': {'gold': 40}, 'msg': '牧场升级申请已提交'},
|
|
('blackmarket', 'trade'): {'cost': {'gold': 25}, 'gain': {'rare': 1}, 'msg': '黑市交易(风险):稀有物资 +1'},
|
|
('blackmarket', 'upgrade'):{'cost': {'gold': 80}, 'msg': '黑市升级申请已提交'},
|
|
# —— 设施(平面)——
|
|
('farmland', 'harvest'): {'gain': {'food': 5}, 'msg': '收获粮食 +5'},
|
|
('farmland', 'sow'): {'cost': {'gold': 5}, 'gain': {'food': 2}, 'msg': '播种:预期粮+2'},
|
|
('farmland', 'upgrade'): {'cost': {'gold': 20}, 'msg': '农田升级申请已提交'},
|
|
('fence', 'repair'): {'cost': {'wood': 2}, 'msg': '栅栏已修缮'},
|
|
('fence', 'upgrade'): {'cost': {'gold': 15}, 'msg': '栅栏升级申请已提交'},
|
|
('wall', 'reinforce'): {'cost': {'stone': 3}, 'gain': {'prestige': 1}, 'msg': '围墙加固,防御+'},
|
|
('wall', 'upgrade'): {'cost': {'gold': 25}, 'msg': '围墙升级申请已提交'},
|
|
('storage', 'deposit'): {'msg': '已打开仓储(在前端存取背包)'},
|
|
('storage', 'upgrade'): {'cost': {'gold': 20}, 'msg': '存储箱升级申请已提交'},
|
|
('well', 'draw'): {'gain': {'water': 3}, 'msg': '取水 +3'},
|
|
('well', 'upgrade'): {'cost': {'gold': 15}, 'msg': '水井升级申请已提交'},
|
|
('market', 'stall'): {'gain': {'gold': 12}, 'msg': '摆摊收入 +12 金币'},
|
|
('market', 'upgrade'): {'cost': {'gold': 25}, 'msg': '集市升级申请已提交'},
|
|
}
|
|
|
|
def _get_world(self):
|
|
user = request.env.user
|
|
return request.env['yt.world'].sudo().search(
|
|
[('user_id', '=', user.id)], limit=1)
|
|
|
|
def _get_char(self, world):
|
|
char = request.env['yt.world.character'].sudo().search(
|
|
[('world_id', '=', world.id)], limit=1)
|
|
if not char:
|
|
char = request.env['yt.world.character'].sudo().create(
|
|
{'world_id': world.id})
|
|
return char
|
|
|
|
@http.route('/yt_world/api/char', type='http', auth='user',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def get_char(self, **kw):
|
|
"""取当前世界角色(不存在则建)。"""
|
|
world = self._get_world()
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
char = self._get_char(world)
|
|
return _json({
|
|
'ok': True,
|
|
'name': char.name, 'level': char.level, 'exp': char.exp,
|
|
'gold': char.gold, 'inventory': char.inventory or {},
|
|
'equip': char.equip or {'weapon': None, 'armor': None, 'accessory': None},
|
|
})
|
|
|
|
@http.route('/yt_world/api/char/save', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def save_char(self, **kw):
|
|
"""保存角色(等级/经验/金币/背包/装备)。body: 部分字段皆可。"""
|
|
world = self._get_world()
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
char = self._get_char(world)
|
|
body = _post_body()
|
|
vals = {}
|
|
for f in ('name', 'level', 'exp', 'gold'):
|
|
if f in body and body[f] is not None:
|
|
vals[f] = body[f]
|
|
if 'inventory' in body and body['inventory'] is not None:
|
|
vals['inventory'] = body['inventory']
|
|
if 'equip' in body and body['equip'] is not None:
|
|
vals['equip'] = body['equip']
|
|
char.sudo().write(vals)
|
|
return _json({'ok': True, 'char': {
|
|
'name': char.name, 'level': char.level, 'exp': char.exp,
|
|
'gold': char.gold, 'inventory': char.inventory or {},
|
|
'equip': char.equip or {},
|
|
}})
|
|
|
|
@http.route('/yt_world/api/creatures', type='http', auth='user',
|
|
methods=['GET', 'OPTIONS'], csrf=False)
|
|
def get_creatures(self, **kw):
|
|
"""取当前世界全部生物。返回含服务器 id 与客户端 cid(partner.cid)。"""
|
|
world = self._get_world()
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
recs = request.env['yt.world.creature'].sudo().search(
|
|
[('world_id', '=', world.id)], order='id')
|
|
out = []
|
|
for r in recs:
|
|
out.append({
|
|
'oid': r.id,
|
|
'cid': (r.partner or {}).get('cid'),
|
|
'name': r.name, 'species': r.species, 'level': r.level,
|
|
'exp': r.exp, 'hp': r.hp, 'hp_max': r.hp_max,
|
|
'energy': r.energy, 'mood': r.mood or '😊', 'job': r.job,
|
|
'equip': r.equip or {'weapon': None, 'armor': None, 'accessory': None},
|
|
'tile_ref': r.tile_ref or {}, 'state': r.state or 'resting',
|
|
})
|
|
return _json({'ok': True, 'creatures': out})
|
|
|
|
@http.route('/yt_world/api/creatures/save', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def save_creatures(self, **kw):
|
|
"""upsert 生物列表。body: {"creatures":[{cid,name,species,level,exp,hp,hp_max,energy,mood,job,equip,tile_ref,state}]}
|
|
按 partner.cid 匹配;存在则更新,否则新建。返回带 oid 的列表。"""
|
|
world = self._get_world()
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
items = body.get('creatures') or []
|
|
model = request.env['yt.world.creature'].sudo()
|
|
out = []
|
|
for c in items:
|
|
cid = c.get('cid')
|
|
vals = {
|
|
'name': c.get('name') or '居民',
|
|
'species': c.get('species'),
|
|
'level': c.get('level', 1), 'exp': c.get('exp', 0),
|
|
'hp': c.get('hp', 100), 'hp_max': c.get('hp_max', 100),
|
|
'energy': c.get('energy', 100), 'mood': c.get('mood', '😊'),
|
|
'job': c.get('job', 'none'),
|
|
'equip': c.get('equip') or {'weapon': None, 'armor': None, 'accessory': None},
|
|
'tile_ref': c.get('tile_ref') or {},
|
|
'state': c.get('state', 'resting'),
|
|
'partner': {'cid': cid},
|
|
}
|
|
# Json 字段不能用 ilike 检索,改为在 Python 侧按 partner.cid 匹配。
|
|
rec = None
|
|
if cid:
|
|
for r in model.search([('world_id', '=', world.id)]):
|
|
if (r.partner or {}).get('cid') == cid:
|
|
rec = r
|
|
break
|
|
if rec:
|
|
rec.write(vals)
|
|
else:
|
|
vals['world_id'] = world.id
|
|
rec = model.create(vals)
|
|
out.append({'oid': rec.id, 'cid': cid, 'level': rec.level,
|
|
'exp': rec.exp, 'hp': rec.hp, 'equip': rec.equip or {}})
|
|
return _json({'ok': True, 'creatures': out})
|
|
|
|
@http.route('/yt_world/api/building/action', type='http', auth='user',
|
|
methods=['POST', 'OPTIONS'], csrf=False)
|
|
def building_action(self, **kw):
|
|
"""执行建筑交互动作。body: {"code","action","q","r","creature_id"}
|
|
数据驱动:查 BUILD_EFFECTS,结算金币/资源,按需回血,持久化角色。"""
|
|
world = self._get_world()
|
|
if not world:
|
|
return _json({'ok': False, 'error': 'no_world'})
|
|
body = _post_body()
|
|
code = body.get('code') or body.get('type')
|
|
action = body.get('action')
|
|
creature_id = body.get('creature_id')
|
|
key = (code, action)
|
|
eff = self.BUILD_EFFECTS.get(key)
|
|
if not eff:
|
|
return _json({'ok': False, 'error': 'unknown_action', 'key': list(key)})
|
|
|
|
char = self._get_char(world)
|
|
inv = dict(char.inventory or {})
|
|
gold = char.gold or 0
|
|
|
|
# 扣除成本
|
|
for k, v in (eff.get('cost') or {}).items():
|
|
if k == 'gold':
|
|
if gold < v:
|
|
return _json({'ok': False, 'error': 'no_gold'})
|
|
gold -= v
|
|
else:
|
|
if inv.get(k, 0) < v:
|
|
return _json({'ok': False, 'error': 'no_res', 'res': k})
|
|
inv[k] = inv.get(k, 0) - v
|
|
# 发放收益
|
|
for k, v in (eff.get('gain') or {}).items():
|
|
if k == 'gold':
|
|
gold += v
|
|
else:
|
|
inv[k] = inv.get(k, 0) + v
|
|
# 治疗生物
|
|
if eff.get('heal') and creature_id:
|
|
cr = request.env['yt.world.creature'].sudo().search(
|
|
[('id', '=', int(creature_id)), ('world_id', '=', world.id)], limit=1)
|
|
if cr:
|
|
cr.hp = cr.hp_max
|
|
# 持久化角色
|
|
char.sudo().write({'gold': gold, 'inventory': inv})
|
|
return _json({
|
|
'ok': True, 'gold': gold, 'inventory': inv,
|
|
'message': eff.get('msg', '操作成功'),
|
|
})
|