fix 解决访问密码,创建会议接口报错,后台docs无法打开问题 新增无数据库与redis 快速运行支持sh
parent
6e347be83b
commit
7c63ec1ebe
|
|
@ -53,6 +53,19 @@ def _process_tags(cursor, tag_string: Optional[str], creator_id: Optional[int] =
|
||||||
tags_data = cursor.fetchall()
|
tags_data = cursor.fetchall()
|
||||||
return [Tag(**tag) for tag in tags_data]
|
return [Tag(**tag) for tag in tags_data]
|
||||||
|
|
||||||
|
def _sync_attendees(cursor, meeting_id: int, attendee_ids: Optional[List[int]]) -> None:
|
||||||
|
attendee_id_list = []
|
||||||
|
if attendee_ids:
|
||||||
|
attendee_id_list = list(dict.fromkeys(int(user_id) for user_id in attendee_ids if user_id is not None))
|
||||||
|
|
||||||
|
cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,))
|
||||||
|
|
||||||
|
if attendee_id_list:
|
||||||
|
cursor.executemany(
|
||||||
|
'INSERT IGNORE INTO attendees (meeting_id, user_id) VALUES (%s, %s)',
|
||||||
|
[(meeting_id, user_id) for user_id in attendee_id_list]
|
||||||
|
)
|
||||||
|
|
||||||
def _get_meeting_overall_status(meeting_id: int) -> dict:
|
def _get_meeting_overall_status(meeting_id: int) -> dict:
|
||||||
"""
|
"""
|
||||||
获取会议的整体进度状态(包含转译和LLM两个阶段)
|
获取会议的整体进度状态(包含转译和LLM两个阶段)
|
||||||
|
|
@ -248,7 +261,7 @@ def get_meetings(
|
||||||
meeting_list.append(Meeting(
|
meeting_list.append(Meeting(
|
||||||
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||||
summary=meeting['summary'], created_at=meeting['created_at'], audio_file_path=meeting['audio_file_path'],
|
summary=meeting['summary'], created_at=meeting['created_at'], audio_file_path=meeting['audio_file_path'],
|
||||||
attendees=attendees, creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags_list,
|
attendees=attendees, attendee_ids=[row['user_id'] for row in attendees_data], creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags_list,
|
||||||
access_password=meeting.get('access_password'),
|
access_password=meeting.get('access_password'),
|
||||||
overall_status=progress_info.get('overall_status'),
|
overall_status=progress_info.get('overall_status'),
|
||||||
overall_progress=progress_info.get('overall_progress'),
|
overall_progress=progress_info.get('overall_progress'),
|
||||||
|
|
@ -319,7 +332,7 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
query = '''
|
query = '''
|
||||||
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags,
|
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags,
|
||||||
m.user_id as creator_id, u.caption as creator_username,
|
m.user_id as creator_id, u.caption as creator_username, m.prompt_id,
|
||||||
af.file_path as audio_file_path, af.duration as audio_duration,
|
af.file_path as audio_file_path, af.duration as audio_duration,
|
||||||
p.name as prompt_name, m.access_password
|
p.name as prompt_name, m.access_password
|
||||||
FROM meetings m
|
FROM meetings m
|
||||||
|
|
@ -341,7 +354,9 @@ def get_meeting_details(meeting_id: int, current_user: dict = Depends(get_curren
|
||||||
meeting_data = Meeting(
|
meeting_data = Meeting(
|
||||||
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||||
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
||||||
|
attendee_ids=[row['user_id'] for row in attendees_data],
|
||||||
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
||||||
|
prompt_id=meeting.get('prompt_id'),
|
||||||
prompt_name=meeting.get('prompt_name'),
|
prompt_name=meeting.get('prompt_name'),
|
||||||
access_password=meeting.get('access_password')
|
access_password=meeting.get('access_password')
|
||||||
)
|
)
|
||||||
|
|
@ -385,17 +400,24 @@ def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = D
|
||||||
# 使用 _process_tags 来处理标签创建
|
# 使用 _process_tags 来处理标签创建
|
||||||
if meeting_request.tags:
|
if meeting_request.tags:
|
||||||
_process_tags(cursor, meeting_request.tags, current_user['user_id'])
|
_process_tags(cursor, meeting_request.tags, current_user['user_id'])
|
||||||
meeting_query = 'INSERT INTO meetings (user_id, title, meeting_time, summary, tags, created_at) VALUES (%s, %s, %s, %s, %s, %s)'
|
meeting_query = '''
|
||||||
cursor.execute(meeting_query, (meeting_request.user_id, meeting_request.title, meeting_request.meeting_time, None, meeting_request.tags, datetime.now().isoformat()))
|
INSERT INTO meetings (user_id, title, meeting_time, summary, tags, prompt_id, created_at)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
|
'''
|
||||||
|
cursor.execute(
|
||||||
|
meeting_query,
|
||||||
|
(
|
||||||
|
current_user['user_id'],
|
||||||
|
meeting_request.title,
|
||||||
|
meeting_request.meeting_time,
|
||||||
|
None,
|
||||||
|
meeting_request.tags,
|
||||||
|
meeting_request.prompt_id or 0,
|
||||||
|
datetime.now().isoformat(),
|
||||||
|
),
|
||||||
|
)
|
||||||
meeting_id = cursor.lastrowid
|
meeting_id = cursor.lastrowid
|
||||||
# 根据 caption 查找用户ID并插入参会人
|
_sync_attendees(cursor, meeting_id, meeting_request.attendee_ids)
|
||||||
if meeting_request.attendees:
|
|
||||||
captions = [c.strip() for c in meeting_request.attendees.split(',') if c.strip()]
|
|
||||||
if captions:
|
|
||||||
placeholders = ','.join(['%s'] * len(captions))
|
|
||||||
cursor.execute(f'SELECT user_id FROM sys_users WHERE caption IN ({placeholders})', captions)
|
|
||||||
for row in cursor.fetchall():
|
|
||||||
cursor.execute('INSERT IGNORE INTO attendees (meeting_id, user_id) VALUES (%s, %s)', (meeting_id, row['user_id']))
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
return create_api_response(code="200", message="Meeting created successfully", data={"meeting_id": meeting_id})
|
return create_api_response(code="200", message="Meeting created successfully", data={"meeting_id": meeting_id})
|
||||||
|
|
||||||
|
|
@ -403,7 +425,7 @@ def create_meeting(meeting_request: CreateMeetingRequest, current_user: dict = D
|
||||||
def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, current_user: dict = Depends(get_current_user)):
|
def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, current_user: dict = Depends(get_current_user)):
|
||||||
with get_db_connection() as connection:
|
with get_db_connection() as connection:
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT user_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
cursor.execute("SELECT user_id, prompt_id FROM meetings WHERE meeting_id = %s", (meeting_id,))
|
||||||
meeting = cursor.fetchone()
|
meeting = cursor.fetchone()
|
||||||
if not meeting:
|
if not meeting:
|
||||||
return create_api_response(code="404", message="Meeting not found")
|
return create_api_response(code="404", message="Meeting not found")
|
||||||
|
|
@ -412,17 +434,20 @@ def update_meeting(meeting_id: int, meeting_request: UpdateMeetingRequest, curre
|
||||||
# 使用 _process_tags 来处理标签创建
|
# 使用 _process_tags 来处理标签创建
|
||||||
if meeting_request.tags:
|
if meeting_request.tags:
|
||||||
_process_tags(cursor, meeting_request.tags, current_user['user_id'])
|
_process_tags(cursor, meeting_request.tags, current_user['user_id'])
|
||||||
update_query = 'UPDATE meetings SET title = %s, meeting_time = %s, summary = %s, tags = %s WHERE meeting_id = %s'
|
update_query = 'UPDATE meetings SET title = %s, meeting_time = %s, summary = %s, tags = %s, prompt_id = %s WHERE meeting_id = %s'
|
||||||
cursor.execute(update_query, (meeting_request.title, meeting_request.meeting_time, meeting_request.summary, meeting_request.tags, meeting_id))
|
cursor.execute(
|
||||||
cursor.execute("DELETE FROM attendees WHERE meeting_id = %s", (meeting_id,))
|
update_query,
|
||||||
# 根据 caption 查找用户ID并插入参会人
|
(
|
||||||
if meeting_request.attendees:
|
meeting_request.title,
|
||||||
captions = [c.strip() for c in meeting_request.attendees.split(',') if c.strip()]
|
meeting_request.meeting_time,
|
||||||
if captions:
|
meeting_request.summary,
|
||||||
placeholders = ','.join(['%s'] * len(captions))
|
meeting_request.tags,
|
||||||
cursor.execute(f'SELECT user_id FROM sys_users WHERE caption IN ({placeholders})', captions)
|
meeting_request.prompt_id if meeting_request.prompt_id is not None else meeting['prompt_id'],
|
||||||
for row in cursor.fetchall():
|
meeting_id,
|
||||||
cursor.execute('INSERT INTO attendees (meeting_id, user_id) VALUES (%s, %s)', (meeting_id, row['user_id']))
|
),
|
||||||
|
)
|
||||||
|
if meeting_request.attendee_ids is not None:
|
||||||
|
_sync_attendees(cursor, meeting_id, meeting_request.attendee_ids)
|
||||||
connection.commit()
|
connection.commit()
|
||||||
# 同步导出总结MD文件
|
# 同步导出总结MD文件
|
||||||
if meeting_request.summary:
|
if meeting_request.summary:
|
||||||
|
|
@ -453,7 +478,7 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre
|
||||||
cursor = connection.cursor(dictionary=True)
|
cursor = connection.cursor(dictionary=True)
|
||||||
query = '''
|
query = '''
|
||||||
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags,
|
SELECT m.meeting_id, m.title, m.meeting_time, m.summary, m.created_at, m.tags,
|
||||||
m.user_id as creator_id, u.caption as creator_username, af.file_path as audio_file_path,
|
m.user_id as creator_id, u.caption as creator_username, m.prompt_id, af.file_path as audio_file_path,
|
||||||
m.access_password
|
m.access_password
|
||||||
FROM meetings m JOIN sys_users u ON m.user_id = u.user_id LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
FROM meetings m JOIN sys_users u ON m.user_id = u.user_id LEFT JOIN audio_files af ON m.meeting_id = af.meeting_id
|
||||||
WHERE m.meeting_id = %s
|
WHERE m.meeting_id = %s
|
||||||
|
|
@ -471,7 +496,9 @@ def get_meeting_for_edit(meeting_id: int, current_user: dict = Depends(get_curre
|
||||||
meeting_data = Meeting(
|
meeting_data = Meeting(
|
||||||
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
meeting_id=meeting['meeting_id'], title=meeting['title'], meeting_time=meeting['meeting_time'],
|
||||||
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
summary=meeting['summary'], created_at=meeting['created_at'], attendees=attendees,
|
||||||
|
attendee_ids=[row['user_id'] for row in attendees_data],
|
||||||
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
creator_id=meeting['creator_id'], creator_username=meeting['creator_username'], tags=tags,
|
||||||
|
prompt_id=meeting.get('prompt_id'),
|
||||||
access_password=meeting.get('access_password')
|
access_password=meeting.get('access_password')
|
||||||
)
|
)
|
||||||
if meeting.get('audio_file_path'):
|
if meeting.get('audio_file_path'):
|
||||||
|
|
@ -1023,7 +1050,7 @@ def get_meeting_navigation(
|
||||||
return create_api_response(code="500", message=f"获取导航信息失败: {str(e)}")
|
return create_api_response(code="500", message=f"获取导航信息失败: {str(e)}")
|
||||||
|
|
||||||
@router.get("/meetings/{meeting_id}/preview-data")
|
@router.get("/meetings/{meeting_id}/preview-data")
|
||||||
def get_meeting_preview_data(meeting_id: int):
|
def get_meeting_preview_data(meeting_id: int, password: Optional[str] = None):
|
||||||
"""
|
"""
|
||||||
获取会议预览数据(无需登录认证)
|
获取会议预览数据(无需登录认证)
|
||||||
用于二维码扫描后的预览页面
|
用于二维码扫描后的预览页面
|
||||||
|
|
@ -1055,6 +1082,32 @@ def get_meeting_preview_data(meeting_id: int):
|
||||||
if not meeting:
|
if not meeting:
|
||||||
return create_api_response(code="404", message="会议不存在")
|
return create_api_response(code="404", message="会议不存在")
|
||||||
|
|
||||||
|
stored_password = (meeting.get('access_password') or '').strip()
|
||||||
|
provided_password = (password or '').strip()
|
||||||
|
|
||||||
|
if stored_password:
|
||||||
|
if not provided_password:
|
||||||
|
return create_api_response(
|
||||||
|
code="401",
|
||||||
|
message="此会议受密码保护",
|
||||||
|
data={
|
||||||
|
"meeting_id": meeting_id,
|
||||||
|
"title": meeting['title'],
|
||||||
|
"requires_password": True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if provided_password != stored_password:
|
||||||
|
return create_api_response(
|
||||||
|
code="401",
|
||||||
|
message="密码错误",
|
||||||
|
data={
|
||||||
|
"meeting_id": meeting_id,
|
||||||
|
"title": meeting['title'],
|
||||||
|
"requires_password": True
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 获取整体进度状态(两阶段)
|
# 获取整体进度状态(两阶段)
|
||||||
progress_info = _get_meeting_overall_status(meeting_id)
|
progress_info = _get_meeting_overall_status(meeting_id)
|
||||||
overall_status = progress_info["overall_status"]
|
overall_status = progress_info["overall_status"]
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ def get_db_connection():
|
||||||
connection = None
|
connection = None
|
||||||
try:
|
try:
|
||||||
connection = mysql.connector.connect(**DATABASE_CONFIG)
|
connection = mysql.connector.connect(**DATABASE_CONFIG)
|
||||||
yield connection
|
|
||||||
except Error as e:
|
except Error as e:
|
||||||
print(f"数据库连接错误: {e}")
|
print(f"数据库连接错误: {e}")
|
||||||
raise HTTPException(status_code=500, detail="数据库连接失败")
|
raise HTTPException(status_code=500, detail="数据库连接失败")
|
||||||
|
try:
|
||||||
|
yield connection
|
||||||
finally:
|
finally:
|
||||||
if connection and connection.is_connected():
|
if connection and connection.is_connected():
|
||||||
try:
|
try:
|
||||||
# 确保清理任何未读结果
|
|
||||||
if connection.unread_result:
|
if connection.unread_result:
|
||||||
connection.consume_results()
|
connection.consume_results()
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class Meeting(BaseModel):
|
||||||
creator_username: str
|
creator_username: str
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
attendees: List[AttendeeInfo]
|
attendees: List[AttendeeInfo]
|
||||||
|
attendee_ids: Optional[List[int]] = None
|
||||||
tags: List[Tag]
|
tags: List[Tag]
|
||||||
audio_file_path: Optional[str] = None
|
audio_file_path: Optional[str] = None
|
||||||
audio_duration: Optional[float] = None
|
audio_duration: Optional[float] = None
|
||||||
|
|
@ -98,7 +99,7 @@ class TranscriptSegment(BaseModel):
|
||||||
class CreateMeetingRequest(BaseModel):
|
class CreateMeetingRequest(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
meeting_time: datetime.datetime
|
meeting_time: datetime.datetime
|
||||||
attendees: str # 逗号分隔的姓名
|
attendee_ids: List[int]
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: Optional[str] = None # 逗号分隔
|
tags: Optional[str] = None # 逗号分隔
|
||||||
prompt_id: Optional[int] = None
|
prompt_id: Optional[int] = None
|
||||||
|
|
@ -106,7 +107,7 @@ class CreateMeetingRequest(BaseModel):
|
||||||
class UpdateMeetingRequest(BaseModel):
|
class UpdateMeetingRequest(BaseModel):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
meeting_time: Optional[datetime.datetime] = None
|
meeting_time: Optional[datetime.datetime] = None
|
||||||
attendees: Optional[str] = None
|
attendee_ids: Optional[List[int]] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
tags: Optional[str] = None
|
tags: Optional[str] = None
|
||||||
summary: Optional[str] = None
|
summary: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,42 @@ server {
|
||||||
proxy_send_timeout 300s;
|
proxy_send_timeout 300s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /docs {
|
||||||
|
proxy_pass http://backend:8000/docs;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /redoc {
|
||||||
|
proxy_pass http://backend:8000/redoc;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /openapi.json {
|
||||||
|
proxy_pass http://backend:8000/openapi.json;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /docs/oauth2-redirect {
|
||||||
|
proxy_pass http://backend:8000/docs/oauth2-redirect;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# 上传文件代理 (使用 ^~ 提高优先级,避免被静态文件正则匹配拦截)
|
# 上传文件代理 (使用 ^~ 提高优先级,避免被静态文件正则匹配拦截)
|
||||||
location ^~ /uploads/ {
|
location ^~ /uploads/ {
|
||||||
proxy_pass http://backend:8000;
|
proxy_pass http://backend:8000;
|
||||||
|
|
@ -57,4 +93,4 @@ server {
|
||||||
location = /50x.html {
|
location = /50x.html {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user }) => {
|
const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null }) => {
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
@ -47,7 +47,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user })
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
title: meeting.title,
|
title: meeting.title,
|
||||||
meeting_time: dayjs(meeting.meeting_time),
|
meeting_time: dayjs(meeting.meeting_time),
|
||||||
attendees: meeting.attendees?.map((a) => (typeof a === 'string' ? a : a.caption)) || [],
|
attendee_ids: meeting.attendee_ids || meeting.attendees?.map((a) => a.user_id).filter(Boolean) || [],
|
||||||
prompt_id: meeting.prompt_id,
|
prompt_id: meeting.prompt_id,
|
||||||
tags: meeting.tags?.map((t) => t.name) || [],
|
tags: meeting.tags?.map((t) => t.name) || [],
|
||||||
description: meeting.description,
|
description: meeting.description,
|
||||||
|
|
@ -66,7 +66,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user })
|
||||||
const payload = {
|
const payload = {
|
||||||
...values,
|
...values,
|
||||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
attendees: values.attendees?.join(',') || '',
|
attendee_ids: values.attendee_ids || [],
|
||||||
tags: values.tags?.join(',') || '',
|
tags: values.tags?.join(',') || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,7 +74,6 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user })
|
||||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), payload);
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meetingId)), payload);
|
||||||
message.success('会议更新成功');
|
message.success('会议更新成功');
|
||||||
} else {
|
} else {
|
||||||
payload.creator_id = user.user_id;
|
|
||||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||||
if (res.code === '200') {
|
if (res.code === '200') {
|
||||||
message.success('会议创建成功');
|
message.success('会议创建成功');
|
||||||
|
|
@ -87,7 +86,7 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user })
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!error?.errorFields) {
|
if (!error?.errorFields) {
|
||||||
message.error(error?.response?.data?.message || '操作失败');
|
message.error(error?.response?.data?.message || error?.response?.data?.detail || '操作失败');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -127,10 +126,10 @@ const MeetingFormDrawer = ({ open, onClose, onSuccess, meetingId = null, user })
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="参会人员" name="attendees" rules={[{ required: true, message: '请选择参会人员' }]}>
|
<Form.Item label="参会人员" name="attendee_ids" rules={[{ required: true, message: '请选择参会人员' }]}>
|
||||||
<Select mode="multiple" placeholder="选择参会人">
|
<Select mode="multiple" placeholder="选择参会人">
|
||||||
{users.map((u) => (
|
{users.map((u) => (
|
||||||
<Select.Option key={u.user_id} value={u.caption}>{u.caption}</Select.Option>
|
<Select.Option key={u.user_id} value={u.user_id}>{u.caption}</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ const API_CONFIG = {
|
||||||
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
||||||
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`,
|
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`,
|
||||||
NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`,
|
NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`,
|
||||||
|
PREVIEW_DATA: (meetingId) => `/api/meetings/${meetingId}/preview-data`,
|
||||||
LLM_MODELS: '/api/llm-models/active'
|
LLM_MODELS: '/api/llm-models/active'
|
||||||
},
|
},
|
||||||
ADMIN: {
|
ADMIN: {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const CreateMeeting = ({ user }) => {
|
const CreateMeeting = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
@ -50,8 +50,7 @@ const CreateMeeting = ({ user }) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
...values,
|
...values,
|
||||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
attendees: values.attendees.join(','),
|
attendee_ids: values.attendee_ids
|
||||||
creator_id: user.user_id
|
|
||||||
};
|
};
|
||||||
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
const res = await apiClient.post(buildApiUrl(API_ENDPOINTS.MEETINGS.CREATE), payload);
|
||||||
if (res.code === '200') {
|
if (res.code === '200') {
|
||||||
|
|
@ -59,7 +58,7 @@ const CreateMeeting = ({ user }) => {
|
||||||
navigate(`/meetings/${res.data.meeting_id}`);
|
navigate(`/meetings/${res.data.meeting_id}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.response?.data?.message || '创建失败');
|
message.error(error.response?.data?.message || error.response?.data?.detail || '创建失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -93,9 +92,9 @@ const CreateMeeting = ({ user }) => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item label="参会人员" name="attendees" rules={[{ required: true, message: '请选择参会人员' }]}>
|
<Form.Item label="参会人员" name="attendee_ids" rules={[{ required: true, message: '请选择参会人员' }]}>
|
||||||
<Select mode="multiple" size="large" placeholder="选择参会人">
|
<Select mode="multiple" size="large" placeholder="选择参会人">
|
||||||
{users.map(u => <Select.Option key={u.user_id} value={u.caption}>{u.caption}</Select.Option>)}
|
{users.map(u => <Select.Option key={u.user_id} value={u.user_id}>{u.caption}</Select.Option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { buildApiUrl, API_ENDPOINTS } from '../config/api';
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const EditMeeting = ({ user }) => {
|
const EditMeeting = () => {
|
||||||
const { meeting_id } = useParams();
|
const { meeting_id } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
|
|
@ -44,7 +44,7 @@ const EditMeeting = ({ user }) => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...meeting,
|
...meeting,
|
||||||
meeting_time: dayjs(meeting.meeting_time),
|
meeting_time: dayjs(meeting.meeting_time),
|
||||||
attendees: meeting.attendees.map(a => typeof a === 'string' ? a : a.caption),
|
attendee_ids: meeting.attendee_ids || meeting.attendees.map(a => a.user_id).filter(Boolean),
|
||||||
tags: meeting.tags?.map(t => t.name) || []
|
tags: meeting.tags?.map(t => t.name) || []
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -60,14 +60,14 @@ const EditMeeting = ({ user }) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
...values,
|
...values,
|
||||||
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
meeting_time: values.meeting_time.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
attendees: values.attendees.join(','),
|
attendee_ids: values.attendee_ids,
|
||||||
tags: values.tags?.join(',') || ''
|
tags: values.tags?.join(',') || ''
|
||||||
};
|
};
|
||||||
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), payload);
|
await apiClient.put(buildApiUrl(API_ENDPOINTS.MEETINGS.UPDATE(meeting_id)), payload);
|
||||||
message.success('会议更新成功');
|
message.success('会议更新成功');
|
||||||
navigate(`/meetings/${meeting_id}`);
|
navigate(`/meetings/${meeting_id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('更新失败');
|
message.error(error.response?.data?.message || error.response?.data?.detail || '更新失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -101,9 +101,9 @@ const EditMeeting = ({ user }) => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Form.Item label="参会人员" name="attendees" rules={[{ required: true, message: '请选择参会人员' }]}>
|
<Form.Item label="参会人员" name="attendee_ids" rules={[{ required: true, message: '请选择参会人员' }]}>
|
||||||
<Select mode="multiple" size="large" placeholder="选择参会人">
|
<Select mode="multiple" size="large" placeholder="选择参会人">
|
||||||
{users.map(u => <Select.Option key={u.user_id} value={u.caption}>{u.caption}</Select.Option>)}
|
{users.map(u => <Select.Option key={u.user_id} value={u.user_id}>{u.caption}</Select.Option>)}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,25 +26,45 @@ const MeetingPreview = () => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [passwordError, setPasswordError] = useState('');
|
||||||
|
const [passwordRequired, setPasswordRequired] = useState(false);
|
||||||
const [isAuthorized, setIsAuthorized] = useState(false);
|
const [isAuthorized, setIsAuthorized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setMeeting(null);
|
||||||
|
setError(null);
|
||||||
|
setPassword('');
|
||||||
|
setPasswordError('');
|
||||||
|
setPasswordRequired(false);
|
||||||
|
setIsAuthorized(false);
|
||||||
fetchPreview();
|
fetchPreview();
|
||||||
}, [meeting_id]);
|
}, [meeting_id]);
|
||||||
|
|
||||||
const fetchPreview = async (pwd = null) => {
|
const fetchPreview = async (pwd = '') => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const url = buildApiUrl(`/api/meetings/preview/${meeting_id}${pwd ? `?password=${pwd}` : ''}`);
|
const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id);
|
||||||
|
const url = buildApiUrl(`${endpoint}${pwd ? `?password=${encodeURIComponent(pwd)}` : ''}`);
|
||||||
const res = await apiClient.get(url);
|
const res = await apiClient.get(url);
|
||||||
setMeeting(res.data);
|
setMeeting(res.data);
|
||||||
setIsAuthorized(true);
|
setIsAuthorized(true);
|
||||||
|
setPasswordRequired(false);
|
||||||
|
setPasswordError('');
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response?.status === 401) {
|
const responseCode = String(err?.response?.data?.code || '');
|
||||||
|
if (responseCode === '401') {
|
||||||
|
setMeeting(null);
|
||||||
setIsAuthorized(false);
|
setIsAuthorized(false);
|
||||||
|
setPasswordRequired(true);
|
||||||
|
setPasswordError(err?.response?.data?.message || '');
|
||||||
|
setError(null);
|
||||||
} else {
|
} else {
|
||||||
setError('无法加载会议预览');
|
setMeeting(null);
|
||||||
|
setIsAuthorized(false);
|
||||||
|
setPasswordRequired(false);
|
||||||
|
setPasswordError('');
|
||||||
|
setError(err?.response?.data?.message || '无法加载会议预览');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -61,7 +81,9 @@ const MeetingPreview = () => {
|
||||||
|
|
||||||
if (loading && !meeting) return <div style={{ textAlign: 'center', padding: '100px' }}><Spin size="large" /></div>;
|
if (loading && !meeting) return <div style={{ textAlign: 'center', padding: '100px' }}><Spin size="large" /></div>;
|
||||||
|
|
||||||
if (!isAuthorized) {
|
if (error) return <Result status="error" title={error} extra={<Link to="/"><Button type="primary" icon={<HomeOutlined />}>返回首页</Button></Link>} />;
|
||||||
|
|
||||||
|
if (passwordRequired && !isAuthorized) {
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#f0f2f5' }}>
|
||||||
<Card style={{ width: 400, borderRadius: 16, textAlign: 'center', boxShadow: '0 8px 24px rgba(0,0,0,0.05)' }}>
|
<Card style={{ width: 400, borderRadius: 16, textAlign: 'center', boxShadow: '0 8px 24px rgba(0,0,0,0.05)' }}>
|
||||||
|
|
@ -73,9 +95,16 @@ const MeetingPreview = () => {
|
||||||
size="large"
|
size="large"
|
||||||
placeholder="访问密码"
|
placeholder="访问密码"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={e => setPassword(e.target.value)}
|
status={passwordError ? 'error' : undefined}
|
||||||
|
onChange={e => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
if (passwordError) {
|
||||||
|
setPasswordError('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
onPressEnter={handleVerify}
|
onPressEnter={handleVerify}
|
||||||
/>
|
/>
|
||||||
|
{passwordError ? <Text type="danger">{passwordError}</Text> : null}
|
||||||
<Button type="primary" size="large" block icon={<EyeOutlined />} onClick={handleVerify}>立即查看</Button>
|
<Button type="primary" size="large" block icon={<EyeOutlined />} onClick={handleVerify}>立即查看</Button>
|
||||||
</Space>
|
</Space>
|
||||||
<div style={{ marginTop: 24 }}>
|
<div style={{ marginTop: 24 }}>
|
||||||
|
|
@ -86,8 +115,6 @@ const MeetingPreview = () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) return <Result status="error" title={error} extra={<Link to="/"><Button type="primary" icon={<HomeOutlined />}>返回首页</Button></Link>} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
|
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
|
||||||
<Header style={{ background: '#fff', padding: '0 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
|
<Header style={{ background: '#fff', padding: '0 24px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', boxShadow: '0 2px 8px rgba(0,0,0,0.05)' }}>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
|
Tag,
|
||||||
Tabs,
|
Tabs,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { DeleteOutlined, EditOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, EditOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_banner() {
|
||||||
|
echo -e "${BLUE}"
|
||||||
|
cat << "EOF"
|
||||||
|
_ __ __ _ _
|
||||||
|
(_) \/ | ___ ___| |_(_)_ __ __ _
|
||||||
|
| | |\/| |/ _ \/ _ \ __| | '_ \ / _` |
|
||||||
|
| | | | | __/ __/ |_| | | | | (_| |
|
||||||
|
|_|_| |_|\___|\___|\__|_|_| |_|\__, |
|
||||||
|
|___/
|
||||||
|
External Middleware Deployment
|
||||||
|
EOF
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
check_dependencies() {
|
||||||
|
print_info "检查系统依赖..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
print_error "未安装 Docker,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||||
|
print_error "未安装 Docker Compose,请先安装 Docker Compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "系统依赖检查通过"
|
||||||
|
}
|
||||||
|
|
||||||
|
init_compose_cmd() {
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
else
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_env_files() {
|
||||||
|
print_info "检查环境变量配置..."
|
||||||
|
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
print_warning ".env 文件不存在,从模板创建..."
|
||||||
|
cp .env.example .env
|
||||||
|
print_warning "请编辑 .env 文件,配置访问端口、BASE_URL、QWEN_API_KEY 等参数"
|
||||||
|
print_warning "按任意键继续,或 Ctrl+C 退出..."
|
||||||
|
read -n 1 -s
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f backend/.env ]; then
|
||||||
|
print_warning "backend/.env 文件不存在,从模板创建..."
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
print_warning "请编辑 backend/.env 文件,配置外部数据库和 Redis 参数"
|
||||||
|
print_warning "按任意键继续,或 Ctrl+C 退出..."
|
||||||
|
read -n 1 -s
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "环境变量文件检查完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
load_external_env() {
|
||||||
|
print_info "加载外部数据库与 Redis 配置..."
|
||||||
|
|
||||||
|
set -a
|
||||||
|
source backend/.env
|
||||||
|
set +a
|
||||||
|
|
||||||
|
DB_PORT="${DB_PORT:-3306}"
|
||||||
|
REDIS_PORT="${REDIS_PORT:-6379}"
|
||||||
|
REDIS_DB="${REDIS_DB:-0}"
|
||||||
|
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
||||||
|
|
||||||
|
local required_vars=(DB_HOST DB_USER DB_PASSWORD DB_NAME REDIS_HOST)
|
||||||
|
for var_name in "${required_vars[@]}"; do
|
||||||
|
if [ -z "${!var_name}" ]; then
|
||||||
|
print_error "backend/.env 缺少必填配置: ${var_name}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
print_success "外部中间件配置已加载"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_directories() {
|
||||||
|
print_info "创建必要的目录..."
|
||||||
|
|
||||||
|
mkdir -p data/uploads
|
||||||
|
mkdir -p data/logs/backend
|
||||||
|
mkdir -p data/logs/frontend
|
||||||
|
mkdir -p backups
|
||||||
|
|
||||||
|
print_success "目录创建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_override_file() {
|
||||||
|
OVERRIDE_FILE="$(mktemp /tmp/imeeting-external-compose.XXXXXX.yml)"
|
||||||
|
trap 'rm -f "$OVERRIDE_FILE"' EXIT
|
||||||
|
|
||||||
|
cat > "$OVERRIDE_FILE" <<EOF
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
environment:
|
||||||
|
DB_HOST: "${DB_HOST}"
|
||||||
|
DB_PORT: "${DB_PORT}"
|
||||||
|
DB_USER: "${DB_USER}"
|
||||||
|
DB_PASSWORD: "${DB_PASSWORD}"
|
||||||
|
DB_NAME: "${DB_NAME}"
|
||||||
|
REDIS_HOST: "${REDIS_HOST}"
|
||||||
|
REDIS_PORT: "${REDIS_PORT}"
|
||||||
|
REDIS_DB: "${REDIS_DB}"
|
||||||
|
REDIS_PASSWORD: "${REDIS_PASSWORD}"
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
start_services() {
|
||||||
|
print_info "启动应用服务(不启动 MySQL/Redis)..."
|
||||||
|
$COMPOSE_CMD -f docker-compose.yml -f "$OVERRIDE_FILE" up -d --build --no-deps backend frontend
|
||||||
|
print_success "应用服务启动命令已执行"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_health() {
|
||||||
|
print_info "等待应用服务启动..."
|
||||||
|
|
||||||
|
local max_wait=120
|
||||||
|
local waited=0
|
||||||
|
|
||||||
|
while [ $waited -lt $max_wait ]; do
|
||||||
|
local healthy_count
|
||||||
|
healthy_count=$($COMPOSE_CMD -f docker-compose.yml -f "$OVERRIDE_FILE" ps --format json 2>/dev/null | grep -c '"Health":"healthy"' || echo "0")
|
||||||
|
|
||||||
|
if [ "$healthy_count" -eq 2 ]; then
|
||||||
|
print_success "前后端服务已就绪"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -ne "\r等待中... (${waited}s/${max_wait}s) 健康: ${healthy_count}/2"
|
||||||
|
sleep 5
|
||||||
|
waited=$((waited + 5))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_warning "服务启动超时,请手动检查状态"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
print_info "服务状态:"
|
||||||
|
$COMPOSE_CMD ps backend frontend
|
||||||
|
}
|
||||||
|
|
||||||
|
show_access_info() {
|
||||||
|
echo ""
|
||||||
|
print_success "==================================="
|
||||||
|
print_success " iMeeting 外部中间件模式部署完成!"
|
||||||
|
print_success "==================================="
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}访问地址:${NC}"
|
||||||
|
echo -e " HTTP访问: ${BLUE}http://localhost${NC}"
|
||||||
|
echo -e " API文档: ${BLUE}http://localhost/docs${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}当前模式:${NC}"
|
||||||
|
echo -e " 仅启动: ${BLUE}backend + frontend${NC}"
|
||||||
|
echo -e " 外部依赖: ${BLUE}backend/.env 中配置的 MySQL / Redis${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}常用命令:${NC}"
|
||||||
|
echo -e " 查看日志: ${BLUE}$COMPOSE_CMD logs -f backend frontend${NC}"
|
||||||
|
echo -e " 停止服务: ${BLUE}./stop-external.sh${NC}"
|
||||||
|
echo -e " 查看状态: ${BLUE}$COMPOSE_CMD ps backend frontend${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
print_banner
|
||||||
|
check_dependencies
|
||||||
|
init_compose_cmd
|
||||||
|
check_env_files
|
||||||
|
load_external_env
|
||||||
|
create_directories
|
||||||
|
create_override_file
|
||||||
|
start_services
|
||||||
|
echo ""
|
||||||
|
wait_for_health
|
||||||
|
echo ""
|
||||||
|
show_status
|
||||||
|
show_access_info
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
if docker compose version &> /dev/null; then
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
else
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "当前应用服务状态:"
|
||||||
|
$COMPOSE_CMD ps backend frontend
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}是否保留应用容器?${NC}"
|
||||||
|
echo "1) 仅停止 backend/frontend(保留容器)"
|
||||||
|
echo "2) 停止并删除 backend/frontend 容器"
|
||||||
|
read -p "请选择 (1/2): " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
print_info "停止应用服务..."
|
||||||
|
$COMPOSE_CMD stop backend frontend
|
||||||
|
print_success "应用服务已停止,外部数据库和 Redis 未受影响"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
print_info "停止并删除应用容器..."
|
||||||
|
$COMPOSE_CMD stop backend frontend
|
||||||
|
$COMPOSE_CMD rm -f backend frontend
|
||||||
|
print_success "应用容器已删除,外部数据库和 Redis 未受影响"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_warning "无效选择,仅停止应用服务"
|
||||||
|
$COMPOSE_CMD stop backend frontend
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue