This commit is contained in:
薇薇安 2026-02-14 14:47:30 +08:00
parent ca0bbeddbf
commit e816524972
6 changed files with 25 additions and 16 deletions

View File

@ -208,7 +208,7 @@ async def start_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""启动交易服务"""
"""启动交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
@ -235,7 +235,7 @@ async def stop_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""停止交易服务"""
"""停止交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
@ -262,7 +262,7 @@ async def restart_service(
account_id: int,
user: Dict[str, Any] = Depends(get_current_user)
):
"""重启交易服务"""
"""重启交易服务(需该账号 owner 或管理员)"""
require_account_owner(account_id, user)
try:
program = program_name_for_account(account_id)
@ -287,9 +287,7 @@ async def restart_service(
async def ensure_trading_program(account_id: int, user: Dict[str, Any] = Depends(get_current_user)):
if int(account_id) <= 0:
raise HTTPException(status_code=400, detail="account_id 必须 >= 1")
# 允许管理员或该账号 owner 执行owner 用于“我重建配置再启动”)
if (user.get("role") or "user") != "admin":
require_account_owner(int(account_id), user)
require_account_owner(int(account_id), user)
sup = ensure_account_program(int(account_id))
if not sup.ok:
raise HTTPException(status_code=500, detail=sup.error or "生成 supervisor 配置失败")

View File

@ -150,6 +150,8 @@ async def grant_user_account(user_id: int, account_id: int, payload: GrantReq, _
if not a:
raise HTTPException(status_code=404, detail="账号不存在")
try:
if payload.role == "owner":
UserAccountMembership.clear_other_owners_for_account(int(account_id), int(user_id))
UserAccountMembership.add(int(user_id), int(account_id), role=payload.role)
except Exception as e:
raise HTTPException(

View File

@ -217,6 +217,14 @@ class UserAccountMembership:
(int(user_id), int(account_id)),
)
@staticmethod
def clear_other_owners_for_account(account_id: int, keep_user_id: int):
"""每个账号仅允许一名 owner将本账号下其他用户的 owner 降为 viewer。"""
db.execute_update(
"UPDATE user_account_memberships SET role = 'viewer' WHERE account_id = %s AND user_id != %s AND role = 'owner'",
(int(account_id), int(keep_user_id)),
)
@staticmethod
def list_for_user(user_id: int):
return db.execute_query(

View File

@ -187,7 +187,7 @@ const UserAccountGroup = ({ user, allAccounts, onServiceAction }) => {
disabled={associating}
>
<option value="viewer">观察者 (Viewer)</option>
<option value="trader">交易员 (Trader)</option>
<option value="owner">拥有者 (Owner每账号仅一个)</option>
</select>
<button
onClick={handleGrant}

View File

@ -310,10 +310,10 @@ const ConfigPanel = () => {
setMessage('')
try {
const res = await api.ensureAccountTradingProgram(accountId)
setMessage(`生成/刷新 supervisor 配置${res.program || ''}`)
setMessage(`成功生成进程配置文件${res.program || ''}`)
await loadAccountTradingStatus()
} catch (error) {
setMessage('生成 supervisor 配置失败: ' + (error.message || '未知错误'))
setMessage('生成进程配置失败:' + (error.message || '未知错误'))
} finally {
setSystemBusy(false)
}
@ -906,7 +906,7 @@ const ConfigPanel = () => {
<p>修改配置后交易系统将在下次扫描时自动使用新配置</p>
</div>
{/* 我的交易进程(按账号;owner/admin 可启停) */}
{/* 我的交易进程(按账号;该账号 owner 或 admin 可启停) */}
{currentAccountMeta && currentAccountMeta.status === 'active' ? (
<div className="system-section">
<div className="system-header">
@ -924,21 +924,21 @@ const ConfigPanel = () => {
<button type="button" className="system-btn primary" onClick={handleTriggerScan} disabled={systemBusy || !accountTradingStatus?.running} title="通知该账号交易进程立即执行一次市场扫描(无需重启)">
立即扫描
</button>
<button type="button" className="system-btn" onClick={handleAccountTradingEnsure} disabled={systemBusy} title="为该账号生成/刷新 supervisor program 配置(需要 owner/admin">
<button type="button" className="system-btn" onClick={handleAccountTradingEnsure} disabled={systemBusy} title="为该账号生成/刷新 supervisor 进程配置文件(需该账号 owner 或管理员">
生成配置
</button>
<button type="button" className="system-btn" onClick={handleAccountTradingStop} disabled={systemBusy || !accountTradingStatus?.running} title="停止该账号交易进程(需要 owner/admin">
<button type="button" className="system-btn" onClick={handleAccountTradingStop} disabled={systemBusy || !accountTradingStatus?.running} title="停止该账号交易进程(需该账号 owner 或管理员">
停止
</button>
<button type="button" className="system-btn" onClick={handleAccountTradingStart} disabled={systemBusy || accountTradingStatus?.running} title="启动该账号交易进程(需要 owner/admin">
<button type="button" className="system-btn" onClick={handleAccountTradingStart} disabled={systemBusy || accountTradingStatus?.running} title="启动该账号交易进程(需该账号 owner 或管理员">
启动
</button>
<button type="button" className="system-btn primary" onClick={handleAccountTradingRestart} disabled={systemBusy} title="重启该账号交易进程(需要 owner/admin">
<button type="button" className="system-btn primary" onClick={handleAccountTradingRestart} disabled={systemBusy} title="重启该账号交易进程(需该账号 owner 或管理员">
重启
</button>
</div>
<div className="system-hint">
提示若按钮报无权限请让管理员在用户授权里把该账号分配为 owner若报 supervisor 相关错误请检查后端`/www/server/panel/plugin/supervisor` 写权限与 supervisorctl 可执行权限
提示生成配置启停/重启进程修改 API 密钥均需该账号的 owner每账号仅一个若报无权限请让管理员将该账号分配为 owner若报 supervisor 相关错误请检查后端写权限与 supervisorctl
</div>
{accountTradingErr ? (
<div className="system-hint" style={{ color: '#b00020' }}>

View File

@ -816,10 +816,11 @@ export const api = {
grantUserAccount: async (userId, accountId, role = 'viewer') => {
const uid = Number(userId);
const aid = Number(accountId);
const roleVal = role === 'owner' ? 'owner' : 'viewer';
const response = await fetch(buildUrl(`/api/admin/users/${uid}/accounts/${aid}`), {
method: 'PUT',
headers: withAuthHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ role: role === 'owner' ? 'owner' : 'viewer' }),
body: JSON.stringify({ role: roleVal }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: '授权失败' }));