Skip to content

Commit 8af0227

Browse files
committed
[feat]: 组织管理基本功能
1 parent 75fd5af commit 8af0227

File tree

7 files changed

+127
-65
lines changed

7 files changed

+127
-65
lines changed

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs
22
SENDER_PASSWORD=TXVU2unpCAE2EtEX
3-
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
3+
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
4+
FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o=
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""优化user_group和group表
2+
3+
Revision ID: fd8714315ad3
4+
Revises: 004c4aa2b3f3
5+
Create Date: 2025-05-23 13:09:52.425623
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
from sqlalchemy.dialects import mysql
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'fd8714315ad3'
16+
down_revision: Union[str, None] = '004c4aa2b3f3'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.drop_table('enter_application')
25+
op.add_column('groups', sa.Column('avatar', sa.String(length=100), nullable=True))
26+
op.add_column('user_group', sa.Column('level', sa.Integer(), nullable=True))
27+
op.drop_column('user_group', 'is_admin')
28+
# ### end Alembic commands ###
29+
30+
31+
def downgrade() -> None:
32+
"""Downgrade schema."""
33+
# ### commands auto generated by Alembic - please adjust! ###
34+
op.add_column('user_group', sa.Column('is_admin', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
35+
op.drop_column('user_group', 'level')
36+
op.drop_column('groups', 'avatar')
37+
op.create_table('enter_application',
38+
sa.Column('user_id', mysql.INTEGER(), autoincrement=False, nullable=False),
39+
sa.Column('group_id', mysql.INTEGER(), autoincrement=False, nullable=False),
40+
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name='enter_application_ibfk_1'),
41+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='enter_application_ibfk_2'),
42+
sa.PrimaryKeyConstraint('user_id', 'group_id'),
43+
mysql_collate='utf8mb4_0900_ai_ci',
44+
mysql_default_charset='utf8mb4',
45+
mysql_engine='InnoDB'
46+
)
47+
# ### end Alembic commands ###

app/api/v1/endpoints/group.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from sqlalchemy.ext.asyncio import AsyncSession
33
from cryptography.fernet import Fernet
44
import os
5-
import glob
5+
import uuid
66
from datetime import date, datetime
77
import json
88

99
from app.utils.get_db import get_db
1010
from app.utils.auth import get_current_user
11-
from app.curd.group import crud_create, crud_gen_invite_code, crud_enter_group, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group, crud_get_basic_info, crud_get_people_info, crud_get_my_level
11+
from app.curd.group import crud_create, crud_gen_invite_code, crud_enter_group, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group, crud_get_basic_info, crud_get_people_info, crud_get_my_level, crud_all_groups
1212
from app.schemas.group import EnterGroup, LeaveGroup
1313

1414
router = APIRouter()
@@ -20,15 +20,16 @@ async def create(group_name: str = Query(...), group_desc: str = Query(...), gro
2020
raise HTTPException(status_code=405, detail="Invalid group name, longer than 30")
2121
if len(group_desc) > 200:
2222
raise HTTPException(status_code=405, detail="Invalid group description, longer than 200")
23-
group_id = await crud_create(user.get("id"), group_name, group_desc, db)
24-
# 存储头像,文件名为 {group_id}.上传文件的扩展名
23+
path = "/lhcos-data/group-avatar/default.png"
24+
# 存储头像,保留扩展名
2525
if group_avatar:
2626
os.makedirs("/lhcos-data/group-avatar", exist_ok=True)
2727
ext = os.path.splitext(group_avatar.filename)[1]
28-
path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}")
28+
path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}"
2929
with open(path, "wb") as f:
3030
content = await group_avatar.read()
3131
f.write(content)
32+
await crud_create(user.get("id"), group_name, group_desc, path, db)
3233
return {"msg": "Group created successfully"}
3334

3435
@router.get("/genInviteCode", response_model=dict)
@@ -72,19 +73,18 @@ async def modify_basic_info(group_id: int = Query(...), group_name: str | None =
7273
raise HTTPException(status_code=405, detail="Invalid group name, longer than 30")
7374
if group_desc and len(group_desc) > 200:
7475
raise HTTPException(status_code=405, detail="Invalid group description, longer than 200")
75-
await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc)
76+
new_path = None
7677
if group_avatar:
7778
os.makedirs("/lhcos-data/group-avatar", exist_ok=True)
78-
# 若之前存储了旧头像,则将其删除;若之前就没头像,则不做处理
79-
old_avatar = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) # 基本名为group_id的文件列表,最多有一个元素
80-
if old_avatar:
81-
os.remove(old_avatar[0])
82-
# 存储新头像,文件名为 {group_id}.上传文件的扩展名
79+
# 存储新头像,保留扩展名
8380
ext = os.path.splitext(group_avatar.filename)[1]
84-
path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}")
85-
with open(path, "wb") as f:
81+
new_path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}"
82+
with open(new_path, "wb") as f:
8683
content = await group_avatar.read()
8784
f.write(content)
85+
old_path = await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc, avatar=new_path)
86+
if group_avatar and old_path != "/lhcos-data/group-avatar/default.png":
87+
os.remove(old_path)
8888
return {"msg": "Basic info modified successfully"}
8989

9090
@router.post("/modifyAdminList", response_model=dict)
@@ -106,10 +106,7 @@ async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), use
106106

107107
@router.get("/getBasicInfo", response_model=dict)
108108
async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
109-
name, desc = await crud_get_basic_info(group_id, db)
110-
find = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*"))
111-
avatar = 'default.png' if not find else find[0].removeprefix("/lhcos-data/group-avatar\\\\")
112-
avatar = '/lhcos-data/group-avatar/' + avatar
109+
name, desc, avatar = await crud_get_basic_info(group_id, db)
113110
return {"avatar": avatar, "name": name, "desc": desc}
114111

115112
@router.get("/getPeopleInfo", response_model=dict)
@@ -121,4 +118,10 @@ async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends
121118
async def get_my_level(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
122119
user_id = user.get("id")
123120
level = await crud_get_my_level(user_id, group_id, db)
124-
return {"level": level}
121+
return {"level": level}
122+
123+
@router.get("/allGroups", response_model=dict)
124+
async def all_groups(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
125+
user_id = user.get("id")
126+
leader, admin, member = await crud_all_groups(user_id, db)
127+
return {"leader": leader, "admin": admin, "member": member}

app/curd/group.py

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from fastapi import HTTPException
22
from sqlalchemy.ext.asyncio import AsyncSession
33
from sqlalchemy import select, insert, delete, update
4-
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group
4+
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin
55

6-
async def crud_create(leader: int, name: str, description: str, db: AsyncSession):
7-
new_group = Group(leader=leader, name=name, description=description)
6+
async def crud_create(leader: int, name: str, description: str, path: str, db: AsyncSession):
7+
new_group = Group(leader=leader, name=name, description=description, avatar=path)
88
db.add(new_group)
9+
await db.flush() # 仅将数据同步到数据库,事务尚未提交,此时 new_group.id 已可用
10+
new_relation = insert(user_group).values(user_id=leader, group_id=new_group.id, level=1)
11+
await db.execute(new_relation)
912
await db.commit()
10-
await db.refresh(new_group)
11-
return new_group.id
1213

1314
async def crud_gen_invite_code(user_email: str, db: AsyncSession):
1415
# 检查邮箱存在性
@@ -20,30 +21,30 @@ async def crud_gen_invite_code(user_email: str, db: AsyncSession):
2021

2122
async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession):
2223
# 检查是否已经在组织内
23-
# 已经是组织leader
24-
query = select(Group).where(Group.id == group_id, Group.leader == user_id)
25-
result = await db.execute(query)
26-
group = result.scalar_one_or_none()
27-
# 已经是组织admin或member
2824
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
2925
result = await db.execute(query)
30-
row = result.first()
31-
if group or row:
32-
raise HTTPException(status_code=408, detail="You are already in the group")
33-
26+
exist = result.first()
27+
if exist:
28+
raise HTTPException(status_code=408, detail="You are already in the group")
3429
new_relation = insert(user_group).values(user_id=user_id, group_id=group_id)
3530
await db.execute(new_relation)
3631
await db.commit()
3732

38-
async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = None, desc: str | None = None):
33+
async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = None, desc: str | None = None, avatar: str | None = None):
34+
query = select(Group.avatar).where(Group.id == id)
35+
result = await db.execute(query)
36+
old_path = result.scalar_one_or_none()
3937
update_data = {}
4038
if name:
4139
update_data["name"] = name
4240
if desc:
4341
update_data["description"] = desc
42+
if avatar:
43+
update_data["avatar"] = avatar
4444
query = update(Group).where(Group.id == id).values(**update_data)
4545
await db.execute(query)
4646
await db.commit()
47+
return old_path
4748

4849
async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, db: AsyncSession):
4950
# 检查组织中是否有该成员
@@ -54,7 +55,10 @@ async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, d
5455
raise HTTPException(status_code=405, detail="User currently not in the group")
5556

5657
# 将该成员设为或取消管理员
57-
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(is_admin=add_admin)
58+
if add_admin:
59+
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=2)
60+
else:
61+
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=3)
5862
await db.execute(query)
5963
await db.commit()
6064

@@ -73,10 +77,10 @@ async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession):
7377
await db.commit()
7478

7579
async def crud_get_basic_info(group_id: int, db: AsyncSession):
76-
query = select(Group.name, Group.description).where(Group.id == group_id)
80+
query = select(Group.name, Group.description, Group.avatar).where(Group.id == group_id)
7781
result = await db.execute(query)
7882
group = result.first()
79-
return group.name, group.description
83+
return group.name, group.description, group.avatar
8084

8185
async def crud_get_people_info(group_id: int, db: AsyncSession):
8286
# 创建者信息
@@ -89,7 +93,7 @@ async def crud_get_people_info(group_id: int, db: AsyncSession):
8993
leader = {"id": user.id, "name": user.username, "avatar": user.avatar}
9094

9195
# 管理者信息
92-
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.is_admin == True)
96+
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 2)
9397
result = await db.execute(query)
9498
admin_ids = result.scalars().all()
9599
query = select(User).where(User.id.in_(admin_ids))
@@ -98,7 +102,7 @@ async def crud_get_people_info(group_id: int, db: AsyncSession):
98102
admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users]
99103

100104
# 普通成员信息
101-
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.is_admin == False)
105+
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 3)
102106
result = await db.execute(query)
103107
member_ids = result.scalars().all()
104108
query = select(User).where(User.id.in_(member_ids))
@@ -109,20 +113,35 @@ async def crud_get_people_info(group_id: int, db: AsyncSession):
109113
return leader, admins, members
110114

111115
async def crud_get_my_level(user_id: int, group_id: int, db: AsyncSession):
112-
# 是否是创建者
113-
query = select(Group.leader).where(Group.id == group_id)
114-
result = await db.execute(query)
115-
leader_id = result.scalar_one_or_none()
116-
if user_id == leader_id:
117-
return 1
118116
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
119117
result = await db.execute(query)
120-
relation = result.first() # relation[0] relation[1] relation[2] 分别为表的第1、2、3列
121-
# 是否是管理员
122-
if relation and relation[2]:
123-
return 2
124-
# 是否是普通成员
118+
relation = result.first()
119+
# 在组织中
125120
if relation:
126-
return 3
127-
# 未加入组织
128-
return 4
121+
return relation[2] # relation[0] relation[1] relation[2] 分别为表的第1、2、3列
122+
# 不在组织中
123+
return 4
124+
125+
async def crud_all_groups(user_id: int, db: AsyncSession):
126+
query = select(Group).where(Group.leader == user_id).order_by(Group.id.desc())
127+
result = await db.execute(query)
128+
groups = result.scalars().all()
129+
leader = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]
130+
131+
query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 2)
132+
result = await db.execute(query)
133+
group_ids = result.scalars().all()
134+
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
135+
result = await db.execute(query)
136+
groups = result.scalars().all()
137+
admin = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]
138+
139+
query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 3)
140+
result = await db.execute(query)
141+
group_ids = result.scalars().all()
142+
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
143+
result = await db.execute(query)
144+
groups = result.scalars().all()
145+
member = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]
146+
147+
return leader, admin, member

app/models/model.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
'user_group', Base.metadata,
99
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
1010
Column('group_id', Integer, ForeignKey('groups.id'), primary_key=True),
11-
Column('is_admin', Boolean, default=False)
12-
)
13-
14-
enter_application = Table(
15-
'enter_application', Base.metadata,
16-
Column('user_id', Integer, ForeignKey('users.id'), primary_key=True),
17-
Column('group_id', Integer, ForeignKey('groups.id'), primary_key=True),
11+
Column('level', Integer, default=3) # 1: leader 2: admin 3:member
1812
)
1913

2014
self_recycle_bin = Table(
@@ -51,6 +45,7 @@ class Group(Base):
5145
leader = Column(Integer)
5246
name = Column(String(30), nullable=False)
5347
description = Column(String(200), nullable=False)
48+
avatar = Column(String(100))
5449
create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间
5550
update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间
5651
users = relationship('User', secondary=user_group, back_populates='groups')

app/schemas/group.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
from pydantic import BaseModel
22

3-
class ApplyToEnter(BaseModel):
4-
group_id: int
5-
63
class LeaveGroup(BaseModel):
74
group_id: int
85

9-
class GetBasicInfo(BaseModel):
10-
group_id: int
6+
class EnterGroup(BaseModel):
7+
inviteCode: str

requirements.txt

421 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)