合并统计,学生,老师,成绩,班级模块,还差顾问,就业
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from dao.wl_statistics_dao import StatisticDao
|
||||
from database import get_db
|
||||
from dao import wl_statistics_dao
|
||||
from scheme.wl_statistics_scheme import (AgeCompareOp, ClassAvgEmpTimeOut,
|
||||
ClassAvgScoreOut, ClassGenderStatOut,
|
||||
EmpTopOut, FailCountModel,
|
||||
ScoreOutLine, SortOrder, EmpTimeOut)
|
||||
from scheme.wl_student_scheme import StudentOut
|
||||
|
||||
router = APIRouter(prefix='/statistics',tags=["统计分析模块"])
|
||||
|
||||
|
||||
# ---- 2.6.1.1 动态年龄范围查询 ----
|
||||
@router.get(
|
||||
"/students/age",
|
||||
response_model=list[StudentOut],
|
||||
summary="动态年龄范围查询",
|
||||
responses={400: {"description": "查询参数不合法,例如区间查询缺少上界 value2"}},
|
||||
)
|
||||
def query_age_range(
|
||||
op: AgeCompareOp = Query(AgeCompareOp.gt, description="比较条件:gt/ge/lt/le/eq/between"),
|
||||
value: int = Query(..., description="年龄阈值"),
|
||||
value2: int | None = Query(None, description="年龄上界,仅 op=between 时必填"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
return StatisticDao.search_students_by_age(db, op, value, value2)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# ---- 2.6.1.2 多维度班级统计 ----
|
||||
@router.get(
|
||||
'/classes/count',
|
||||
summary="多维度班级统计",
|
||||
response_model=list[ClassGenderStatOut]
|
||||
)
|
||||
def query_classes_count(db: Session = Depends(get_db)):
|
||||
return StatisticDao.statistic_classes_count(db)
|
||||
|
||||
# 查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩
|
||||
@router.get(
|
||||
'/scores/out_score_line/{score_line}',
|
||||
summary='统计每次超分数线的学生',
|
||||
response_model=list[ScoreOutLine]
|
||||
)
|
||||
def query_out_score_line(score_line:float,db:Session=Depends(get_db)):
|
||||
return StatisticDao.query_score_by_line(db, score_line)
|
||||
|
||||
# 查询有输入指定次数(如两次)以上不及格的学生的姓名、班级和不及格成绩明细
|
||||
@router.get(
|
||||
'/scores/fail_score/{fail_count}',
|
||||
summary='统计不及格次数超过指定次数的学生信息',
|
||||
response_model=list[FailCountModel]
|
||||
)
|
||||
def query_info_by_fail_count(fail_count:int,db: Session = Depends(get_db)):
|
||||
return StatisticDao.query_by_fail_count(db,fail_count)
|
||||
|
||||
# 统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序
|
||||
@router.get(
|
||||
'/scores/get_class_avg_score',
|
||||
summary='统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序',
|
||||
response_model=list[ClassAvgScoreOut]
|
||||
)
|
||||
def get_class_avg_score(
|
||||
sort_order: SortOrder | None = Query(
|
||||
None, description="排序方向:asc 从低到高,desc 从高到低;不传则按班级+考试序次"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return StatisticDao.get_class_avg_score(db, sort_order)
|
||||
|
||||
# 统计就业薪资排名Top N(动态输入N)的学生的姓名、班级、就业时间和就业公司
|
||||
@router.get(
|
||||
'/emp/top_rank/{rank}',
|
||||
summary='统计就业薪资排名Top N',
|
||||
response_model=list[EmpTopOut]
|
||||
)
|
||||
def get_top_rank(
|
||||
rank: int = Path(..., gt=0, description="取薪资排名前 N 名"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return StatisticDao.get_info_by_rank(db, rank)
|
||||
|
||||
# 统计每个学生的就业时长(计算公式:offer下发时间-就业开放时间)
|
||||
@router.get(
|
||||
'/emp/emp_time',
|
||||
summary='统计每个学生的就业时长',
|
||||
response_model=list[EmpTimeOut]
|
||||
)
|
||||
def get_emp_time(db: Session = Depends(get_db)):
|
||||
return StatisticDao.get_emp_time(db)
|
||||
|
||||
# 统计每个班级的平均就业时长(仅统计已进入就业阶段且已拿到 offer 的学生)
|
||||
@router.get(
|
||||
'/emp/class_avg_emp_time',
|
||||
summary='统计每个班级的平均就业时长',
|
||||
response_model=list[ClassAvgEmpTimeOut]
|
||||
)
|
||||
def get_class_avg_emp_time(
|
||||
sort_order: SortOrder | None = Query(
|
||||
None, description="排序方向:asc 从低到高,desc 从高到低;不传则按班级编号"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return StatisticDao.get_class_avg_emp_time(db, sort_order)
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#统计分析 数据访问层(动态查询 + 聚合)
|
||||
from os.path import join
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import and_, case, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from model.wl_class_model import Class_
|
||||
from model.wl_emp_model import Emp
|
||||
from model.wl_score_model import Score
|
||||
from model.wl_student_model import Student
|
||||
from scheme.wl_statistics_scheme import AgeCompareOp, SortOrder
|
||||
|
||||
# stu_gender 字段的实际取值,确认数据库里存的到底是什么后改这两行即可
|
||||
MALE_VALUE = "男"
|
||||
FEMALE_VALUE = "女"
|
||||
|
||||
|
||||
|
||||
|
||||
# ---- 2.6.1.1 动态年龄范围查询 ----
|
||||
# 比较条件 -> SQLAlchemy 表达式工厂,动态拼接 where 条件
|
||||
_AGE_OP_FACTORY = {
|
||||
AgeCompareOp.gt: lambda v1, v2: Student.stu_age > v1,
|
||||
AgeCompareOp.ge: lambda v1, v2: Student.stu_age >= v1,
|
||||
AgeCompareOp.lt: lambda v1, v2: Student.stu_age < v1,
|
||||
AgeCompareOp.le: lambda v1, v2: Student.stu_age <= v1,
|
||||
AgeCompareOp.eq: lambda v1, v2: Student.stu_age == v1,
|
||||
AgeCompareOp.between: lambda v1, v2: Student.stu_age.between(v1, v2),
|
||||
}
|
||||
|
||||
class StatisticDao:
|
||||
|
||||
@staticmethod
|
||||
def search_students_by_age(db: Session, op: AgeCompareOp,
|
||||
value: int, value2: Optional[int] = None):
|
||||
if op == AgeCompareOp.between:
|
||||
if value2 is None:
|
||||
raise ValueError("区间查询(between)需要同时提供上界 value2")
|
||||
if value > value2:
|
||||
raise ValueError("区间查询的下界不能大于上界")
|
||||
|
||||
condition = _AGE_OP_FACTORY[op](value, value2)
|
||||
return db.query(Student) \
|
||||
.filter(Student.is_deleted == 0, condition) \
|
||||
.order_by(Student.stu_age) \
|
||||
.all()
|
||||
@staticmethod
|
||||
def statistic_classes_count(db: Session):
|
||||
"""每个班级的总人数 + 按性别细分的分布(含 0 人的空班级)"""
|
||||
result=[]
|
||||
for c in db.query(Class_).filter(Class_.is_deleted==0).all():
|
||||
students=[s for s in c.students if s.is_deleted == 0]
|
||||
result.append({
|
||||
"class_id":c.class_id,
|
||||
"total":len(students),
|
||||
"male_count":sum(1 for s in students if s.stu_gender==MALE_VALUE),
|
||||
"female_count":sum(1 for s in students if s.stu_gender==FEMALE_VALUE)
|
||||
})
|
||||
return result
|
||||
# 另一个版本的写法,效率更高
|
||||
# male = func.sum(case((Student.stu_gender == MALE_VALUE, 1), else_=0))
|
||||
# female = func.sum(case((Student.stu_gender == FEMALE_VALUE, 1), else_=0))
|
||||
#
|
||||
# return db.query(
|
||||
# WlClass.class_id.label("class_id"),
|
||||
# func.count(Student.stu_id).label("total"),
|
||||
# male.label("male_count"),
|
||||
# female.label("female_count"),
|
||||
# ).select_from(WlClass) \
|
||||
# .outerjoin(
|
||||
# Student,
|
||||
# # 逻辑删除的过滤条件必须写在 ON 里,写进 where 会把空班级整行滤掉
|
||||
# and_(Student.class_id == WlClass.class_id, Student.is_deleted == 0),
|
||||
# ) \
|
||||
# .filter(WlClass.is_deleted == 0) \
|
||||
# .group_by(WlClass.class_id) \
|
||||
# .order_by(WlClass.class_id) \
|
||||
# .all()
|
||||
|
||||
@staticmethod
|
||||
def query_score_by_line(db:Session,score_line:float):
|
||||
result=[]
|
||||
for s in db.query(Student).filter(Student.is_deleted==0).all():
|
||||
if all(sc.score>score_line for sc in s.scores):
|
||||
result.append({
|
||||
"stu_no":s.stu_no,
|
||||
"stu_name":s.stu_name,
|
||||
"scores": [{"exam_order":i.exam_order,"score":i.score} for i in s.scores]
|
||||
})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def query_by_fail_count(db:Session,fail_count:int):
|
||||
result=[]
|
||||
students=db.query(Student).filter(Student.is_deleted==0).all()
|
||||
for stu in students:
|
||||
if len([sc for sc in stu.scores if sc.score<60])>fail_count:
|
||||
result.append({
|
||||
"stu_name":stu.stu_name,
|
||||
"class_id":stu.class_id,
|
||||
"scores":[{"exam_order":i.exam_order,"score":i.score} for i in stu.scores]
|
||||
})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_class_avg_score(db: Session, sort_order: SortOrder | None = None):
|
||||
"""每次考试每个班级的平均分,支持按平均分动态升/降序"""
|
||||
avg_score = func.avg(Score.score).label("avg_score")
|
||||
|
||||
q = db.query(
|
||||
Student.class_id.label("class_id"),
|
||||
Score.exam_order.label("exam_order"),
|
||||
avg_score,
|
||||
).select_from(Score) \
|
||||
.join(Score.student) \
|
||||
.filter(Student.is_deleted == 0, Student.class_id.isnot(None)) \
|
||||
.group_by(Student.class_id, Score.exam_order)
|
||||
|
||||
# 动态排序:传了方向就按平均分排,没传则按班级 + 考试序次排
|
||||
if sort_order == SortOrder.desc:
|
||||
q = q.order_by(avg_score.desc())
|
||||
elif sort_order == SortOrder.asc:
|
||||
q = q.order_by(avg_score.asc())
|
||||
else:
|
||||
q = q.order_by(Student.class_id, Score.exam_order)
|
||||
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_info_by_rank(db:Session,rank:int):
|
||||
q=(db.query(
|
||||
Student.stu_name,
|
||||
Student.class_id,
|
||||
Emp.offer_time,
|
||||
Emp.company_name
|
||||
).select_from(Student) \
|
||||
.join(Student.emp)) \
|
||||
.filter(Student.is_deleted==0) \
|
||||
.order_by(Emp.salary.desc())\
|
||||
.limit(rank)
|
||||
|
||||
return q.all()
|
||||
@staticmethod
|
||||
def get_emp_time(db:Session):
|
||||
"""每个学生的就业时长(天)= offer下发时间 - 就业开放时间
|
||||
"""
|
||||
emp_total_time = func.coalesce(
|
||||
# 未就业学生左外连接后 Emp 列为 NULL,DATEDIFF 也返回 NULL,统一兜成 0
|
||||
func.datediff(Emp.offer_time, Emp.emp_open_time),
|
||||
0,
|
||||
).label('emp_total_time')
|
||||
|
||||
q=db.query(
|
||||
Student.stu_name,
|
||||
emp_total_time,
|
||||
).select_from(Student) \
|
||||
.outerjoin(Student.emp) \
|
||||
.filter(Student.is_deleted==0)
|
||||
|
||||
return q.all()
|
||||
|
||||
@staticmethod
|
||||
def get_class_avg_emp_time(db:Session, sort_order: SortOrder | None = None):
|
||||
"""每个班级的平均就业时长(天)= offer下发时间 - 就业开放时间
|
||||
|
||||
口径:
|
||||
- 以班级表为主表左外连接,保证没有任何就业学生的班级也出现在结果里
|
||||
- 只统计已进入就业阶段(emp_open_time 非空)且已拿到 offer(offer_time 非空)的学生
|
||||
- 该班可统计的人数为 0 时 AVG 返回 NULL,出参里 avg_emp_time 给 None,即"无就业学生"
|
||||
"""
|
||||
emp_time = func.datediff(Emp.offer_time, Emp.emp_open_time)
|
||||
# AVG 和 COUNT(表达式) 都会自动忽略 NULL,所以没 offer 时间的学生不参与分子也不占分母
|
||||
avg_emp_time = func.avg(emp_time).label("avg_emp_time")
|
||||
# 用 COUNT(表达式) 而不是 COUNT(*),它正好是平均值的分母
|
||||
emp_stu_count = func.count(emp_time).label("emp_stu_count")
|
||||
|
||||
q = db.query(
|
||||
Class_.class_id.label("class_id"),
|
||||
avg_emp_time,
|
||||
emp_stu_count,
|
||||
).select_from(Class_) \
|
||||
.outerjoin(
|
||||
Student,
|
||||
# 逻辑删除的过滤条件必须写在 ON 里,写进 where 会把空班级整行滤掉
|
||||
and_(Student.class_id == Class_.class_id, Student.is_deleted == 0),
|
||||
) \
|
||||
.outerjoin(
|
||||
Emp,
|
||||
and_(
|
||||
Emp.stu_id == Student.stu_id,
|
||||
Emp.emp_open_time.isnot(None),
|
||||
Emp.offer_time.isnot(None),
|
||||
),
|
||||
) \
|
||||
.group_by(Class_.class_id)
|
||||
|
||||
# 动态排序逻辑与 get_class_avg_score 保持一致
|
||||
if sort_order == SortOrder.desc:
|
||||
# MySQL 里 DESC 时 NULL 排最后,"无就业学生"的班级自然落到末尾
|
||||
q = q.order_by(avg_emp_time.desc())
|
||||
elif sort_order == SortOrder.asc:
|
||||
q = q.order_by(avg_emp_time.asc())
|
||||
else:
|
||||
q = q.order_by(Class_.class_id)
|
||||
|
||||
return [
|
||||
{
|
||||
"class_id": r.class_id,
|
||||
# AVG 出来是 Decimal,转 float 并按天保留 1 位小数;None 表示该班无就业学生
|
||||
"avg_emp_time": round(float(r.avg_emp_time), 1)
|
||||
if r.avg_emp_time is not None else None,
|
||||
"emp_stu_count": r.emp_stu_count,
|
||||
}
|
||||
for r in q.all()
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ from sqlalchemy.orm import sessionmaker,declarative_base
|
||||
# 1. 配置 MySQL 数据库连接 URL
|
||||
# 格式:mysql+pymysql://用户名:密码@主机:端口/数据库名?编码
|
||||
# 请将下面的 'root', '123456', 'localhost', '3306', 'test_db' 替换为你自己的实际信息
|
||||
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:123456@localhost:3306/test_db"
|
||||
SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:123456@localhost:3306/wl_student_manager_system"
|
||||
|
||||
# 2. 创建数据库引擎
|
||||
# - pool_pre_ping=True 表示每次从连接池取出连接前先 ping 一下,防止使用已断开的连接
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#项目初始化入口
|
||||
from fastapi import FastAPI
|
||||
# 周学灵、熊浩钦、圣国伟、刘盼的路由
|
||||
from api import wl_student_api,wl_class_api,wl_score_api,wl_teacher_api
|
||||
# 周学灵、熊浩钦、圣国伟、刘盼、彭少的路由
|
||||
from api import wl_student_api,wl_class_api,wl_score_api,wl_teacher_api,statistics
|
||||
from database import Base, engine
|
||||
from model import wl_student_model,wl_class_model,wl_advisor_model,wl_score_model,wl_teacher_model
|
||||
|
||||
|
||||
Base.metadata.drop_all(engine)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
app = FastAPI(title="沃林学生管理系统")
|
||||
@@ -13,6 +13,9 @@ app.include_router(wl_student_api.router)
|
||||
app.include_router(wl_class_api.router)
|
||||
app.include_router(wl_score_api.router)
|
||||
app.include_router(wl_teacher_api.router)
|
||||
app.include_router(statistics.router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from sqlalchemy import Column, Integer, Date, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
class Class_(Base):
|
||||
@@ -6,5 +8,7 @@ class Class_(Base):
|
||||
|
||||
class_id = Column(Integer, primary_key=True, autoincrement=True, comment="班级编号")
|
||||
start_time = Column(Date, comment="开课时间")
|
||||
|
||||
students=relationship("Student", back_populates="class_")
|
||||
# 逻辑删除标记(0代表正常,1代表已删除)
|
||||
is_deleted = Column(Integer, default=0, comment="逻辑删除标记")
|
||||
@@ -1,18 +1,20 @@
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.orm import *
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class Emp(Base):
|
||||
__tablename__ = "wl_emp"
|
||||
|
||||
stu_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
stu_id = Column(Integer,ForeignKey('wl_student.stu_id'), primary_key=True, autoincrement=True)
|
||||
emp_open_time = Column(DateTime, default="2026-11-30 18:18:18")
|
||||
offer_time = Column(DateTime, default=func.now())
|
||||
company_name = Column(String(100), nullable=False)
|
||||
salary = Column(Integer, nullable=False, default=15000)
|
||||
|
||||
student = relationship("wl_student", back_populates="wl_emp")
|
||||
# Student.emp 用的是 back_populates="student",这个反向属性必须保留,否则 mapper 配置失败
|
||||
student = relationship("Student", back_populates="emp")
|
||||
|
||||
def __repr__(self):
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, Float
|
||||
from sqlalchemy import Column, Integer, Float, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
class Score(Base):
|
||||
__tablename__ = "wl_score"
|
||||
|
||||
stu_id = Column(Integer, primary_key=True, comment="学生编号")
|
||||
stu_id = Column(Integer, ForeignKey('wl_student.stu_id'),primary_key=True, comment="学生编号")
|
||||
exam_order = Column(Integer, primary_key=True, comment="考核序次")
|
||||
score = Column(Float, nullable=False, comment="成绩")
|
||||
|
||||
student=relationship("Student",back_populates="scores")
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ class Student(Base):
|
||||
stu_age = Column(Integer, nullable=True, comment='年龄')
|
||||
stu_gender = Column(String(8), nullable=True, comment='性别')
|
||||
|
||||
# class_ = relationship("Class", back_populates="students")
|
||||
class_ = relationship("Class_", back_populates="students")
|
||||
scores=relationship("Score",back_populates="student")
|
||||
emp=relationship("Emp",back_populates="student")
|
||||
# advisor = relationship("Advisor", back_populates="students")
|
||||
|
||||
is_deleted = Column(Integer, default=0, comment='逻辑删除: 0正常 1删除')
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#统计分析模块 入参/出参定义
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from model.wl_score_model import Score
|
||||
|
||||
|
||||
# ---- 入参:动态查询的比较条件 ----
|
||||
class AgeCompareOp(str, Enum):
|
||||
"""年龄比较条件:大于/大于等于/小于/小于等于/等于/区间"""
|
||||
gt = "gt"
|
||||
ge = "ge"
|
||||
lt = "lt"
|
||||
le = "le"
|
||||
eq = "eq"
|
||||
between = "between"
|
||||
|
||||
|
||||
class SortOrder(str, Enum):
|
||||
"""排序方向:升序/降序"""
|
||||
asc = "asc"
|
||||
desc = "desc"
|
||||
|
||||
|
||||
# ---- 出参:后续统计接口的聚合结果 DTO 放这里 ----
|
||||
class ClassGenderStatOut(BaseModel):
|
||||
"""班级多维度统计:总人数 + 男女分布"""
|
||||
class_id: Optional[int] = None
|
||||
total: int = 0
|
||||
male_count: int = 0
|
||||
female_count: int = 0
|
||||
|
||||
class ScoreShow(BaseModel):
|
||||
exam_order:int=0
|
||||
score:float=0
|
||||
|
||||
class ScoreOutLine(BaseModel):
|
||||
stu_no:str=''
|
||||
stu_name:str=''
|
||||
scores:List[ScoreShow]=[]
|
||||
|
||||
class FailCountModel(BaseModel):
|
||||
stu_name:str=''
|
||||
class_id:int=0
|
||||
scores:List[ScoreShow]=[]
|
||||
|
||||
class ClassAvgScoreOut(BaseModel):
|
||||
"""每次考试每个班级的平均分"""
|
||||
# DAO 返回的是 SQLAlchemy Row 对象,必须开 from_attributes 才能按属性名读取
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class_id:Optional[int]=None
|
||||
exam_order:Optional[int]=None
|
||||
avg_score:Optional[float]=None
|
||||
|
||||
class EmpTopOut(BaseModel):
|
||||
"""就业薪资排名 Top N"""
|
||||
# 同样是 Row 对象,必须开 from_attributes
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
stu_name:Optional[str]=None
|
||||
class_id:Optional[int]=None
|
||||
offer_time:Optional[datetime]=None
|
||||
company_name:Optional[str]=None
|
||||
|
||||
class ClassAvgEmpTimeOut(BaseModel):
|
||||
"""每个班级的平均就业时长"""
|
||||
class_id: Optional[int] = None
|
||||
# 单位:天,保留 1 位小数;为 None 表示该班无就业学生,前端展示"无就业学生"
|
||||
avg_emp_time: Optional[float] = None
|
||||
# 参与统计的人数(平均值的分母),为 0 同样表示该班无就业学生
|
||||
emp_stu_count: int = 0
|
||||
|
||||
class EmpTimeOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
stu_name:str=''
|
||||
emp_total_time:int=0 # 单位为天
|
||||
Reference in New Issue
Block a user