fix:人员添加职级
This commit is contained in:
parent
d8054366b1
commit
809cdbe3f1
|
|
@ -1,6 +1,6 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api import audit, auth, chat, documents, files, permissions, roles, stats, units, users
|
from app.api import audit, auth, chat, documents, files, job_titles, permissions, roles, stats, units, users
|
||||||
|
|
||||||
api_router = APIRouter(prefix="/api")
|
api_router = APIRouter(prefix="/api")
|
||||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
|
|
@ -8,6 +8,7 @@ api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||||
api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||||
api_router.include_router(permissions.router, prefix="/permissions", tags=["permissions"])
|
api_router.include_router(permissions.router, prefix="/permissions", tags=["permissions"])
|
||||||
api_router.include_router(units.router, prefix="/units", tags=["units"])
|
api_router.include_router(units.router, prefix="/units", tags=["units"])
|
||||||
|
api_router.include_router(job_titles.router, prefix="/job-titles", tags=["job-titles"])
|
||||||
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
api_router.include_router(chat.router, prefix="/chat", tags=["chat"])
|
||||||
api_router.include_router(documents.router, prefix="/documents", tags=["documents"])
|
api_router.include_router(documents.router, prefix="/documents", tags=["documents"])
|
||||||
api_router.include_router(audit.router, prefix="/audit-logs", tags=["audit"])
|
api_router.include_router(audit.router, prefix="/audit-logs", tags=["audit"])
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ def _build_current_user(db: Session, user: User) -> CurrentUserResponse:
|
||||||
phone=user.phone,
|
phone=user.phone,
|
||||||
name=user.name,
|
name=user.name,
|
||||||
department=user.department,
|
department=user.department,
|
||||||
|
job_title=user.job_title,
|
||||||
unit_id=user.unit_id,
|
unit_id=user.unit_id,
|
||||||
unit_name=user.unit.name if user.unit else None,
|
unit_name=user.unit.name if user.unit else None,
|
||||||
status=user.status,
|
status=user.status,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from app.schemas.chat import (
|
||||||
)
|
)
|
||||||
from app.services.audit_service import write_audit
|
from app.services.audit_service import write_audit
|
||||||
from app.services.document_service import resolve_chat_doc_scope
|
from app.services.document_service import resolve_chat_doc_scope
|
||||||
|
from app.services.user_service import JOB_TITLE_GROUP_ALIASES
|
||||||
|
|
||||||
|
|
||||||
def _attachment_data_url(att: Attachment) -> str | None:
|
def _attachment_data_url(att: Attachment) -> str | None:
|
||||||
|
|
@ -179,11 +180,10 @@ def stream_chat(
|
||||||
)
|
)
|
||||||
allowed_tags = [] if current_user.is_superuser else [role.code for role in current_user.roles]
|
allowed_tags = [] if current_user.is_superuser else [role.code for role in current_user.roles]
|
||||||
|
|
||||||
if current_user.is_superuser:
|
# role_hint 必须是业务岗位(如"总院中层副职""员工"),用来匹配制度里按职级分组的条款。
|
||||||
role_labels: list[str] = ["系统管理员"]
|
# 只取 User.job_title;权限角色(employee/admin/system_admin)和 is_superuser 不参与,
|
||||||
else:
|
# 否则会把"系统管理员"塞进制度职级匹配, LLM 找不到对应分组就反问澄清。
|
||||||
role_labels = [role.name for role in current_user.roles if role.name]
|
role_hint = (current_user.job_title or "").strip()
|
||||||
role_hint = "、".join(role_labels)
|
|
||||||
|
|
||||||
# 按用户所属单位(含逐级向上回退)计算允许引用的文档 title 集合。
|
# 按用户所属单位(含逐级向上回退)计算允许引用的文档 title 集合。
|
||||||
# 超管/管理员或用户没绑定单位 -> 不做过滤(allowed_titles=None)。
|
# 超管/管理员或用户没绑定单位 -> 不做过滤(allowed_titles=None)。
|
||||||
|
|
@ -249,14 +249,24 @@ def stream_chat(
|
||||||
|
|
||||||
raw_question = payload.question or ""
|
raw_question = payload.question or ""
|
||||||
hint_lines: list[str] = []
|
hint_lines: list[str] = []
|
||||||
|
# 引用格式规则始终注入,与提问人职位无关。
|
||||||
|
# finance_assistant.md 系统 prompt 里写着"引用由系统自动展示,不用手写",这里覆盖它;
|
||||||
|
# 独立成条防止 job_title=None 的账号(如系统管理员)拿不到规则,导致回答漏"依据文档"。
|
||||||
|
hint_lines.append(
|
||||||
|
"【引用格式】不要在正文开头写\"根据《XX规定》...\"这类引用开场;"
|
||||||
|
"把制度出处放在整段回答的最末尾,另起一行以\"依据文档:《文档全名》(文号,如有)\"的格式列出;"
|
||||||
|
"引用多份文档时每份单独一行。"
|
||||||
|
"此规则最高优先级:即使命中 FAQ/标准答案,或已在正文其他位置提到制度名,末尾仍必须补上此行;"
|
||||||
|
"若知识库无对应制度可引(纯操作步骤、报错处置、闲聊等),则写\"依据文档:无\"。"
|
||||||
|
)
|
||||||
if role_hint:
|
if role_hint:
|
||||||
|
aliases = JOB_TITLE_GROUP_ALIASES.get(role_hint, [role_hint])
|
||||||
|
alias_list = "、".join(f"\"{a}\"" for a in aliases)
|
||||||
hint_lines.append(
|
hint_lines.append(
|
||||||
f"【提问人职位:{role_hint}】请严格遵守以下输出规则:\n"
|
f"【提问人职位:{role_hint}】请严格遵守以下输出规则:\n"
|
||||||
f"1) 若制度按职级/岗位分组列条款(如\"总院领导班子正职\"\"总院中层副职\"\"总助级\"等),"
|
f"1) 若制度按职级/岗位分组列条款(如\"总院领导班子正职\"\"总院中层副职\"\"总助级\"等),"
|
||||||
f"只输出分组名中包含\"{role_hint}\"字样的分组细则;不匹配的分组一律不要列出,也不要用于对比。\n"
|
f"只输出分组名中包含以下任一关键词的分组细则: {alias_list};不匹配的分组一律不要列出,也不要用于对比。\n"
|
||||||
f"2) 若制度不按职级分组,或该职位不涉及相关业务,请直接输出通用条款或明确说明\"该职位无相关规定\"。\n"
|
f"2) 若制度不按职级分组,或该职位不涉及相关业务,请直接输出通用条款或明确说明\"该职位无相关规定\"。"
|
||||||
f"3) 不要在正文开头写\"根据《XX规定》...\"这类引用开场;把制度出处放在整段回答的最末尾,"
|
|
||||||
f"另起一行以\"依据文档:《文档全名》(文号,如有)\"的格式列出;引用多份文档时每份单独一行。"
|
|
||||||
)
|
)
|
||||||
own_unit = scope.get("own_unit") or ""
|
own_unit = scope.get("own_unit") or ""
|
||||||
chosen_unit = scope.get("chosen_unit") or ""
|
chosen_unit = scope.get("chosen_unit") or ""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.db import get_db
|
||||||
|
from app.deps import get_current_user
|
||||||
|
from app.models import JobTitle, User
|
||||||
|
from app.schemas.job_title import JobTitleResponse
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=list[JobTitleResponse])
|
||||||
|
def list_job_titles(
|
||||||
|
_: User = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> list[JobTitleResponse]:
|
||||||
|
items = db.scalars(
|
||||||
|
select(JobTitle).where(JobTitle.is_active == True).order_by(JobTitle.sort_order, JobTitle.id) # noqa: E712
|
||||||
|
).all()
|
||||||
|
return [
|
||||||
|
JobTitleResponse(id=jt.id, name=jt.name, sort_order=jt.sort_order, is_active=jt.is_active)
|
||||||
|
for jt in items
|
||||||
|
]
|
||||||
|
|
@ -23,6 +23,7 @@ def serialize_user(user: User) -> UserResponse:
|
||||||
phone=user.phone,
|
phone=user.phone,
|
||||||
name=user.name,
|
name=user.name,
|
||||||
department=user.department,
|
department=user.department,
|
||||||
|
job_title=user.job_title,
|
||||||
unit_id=user.unit_id,
|
unit_id=user.unit_id,
|
||||||
unit_name=user.unit.name if user.unit else None,
|
unit_name=user.unit.name if user.unit else None,
|
||||||
status=user.status,
|
status=user.status,
|
||||||
|
|
@ -72,6 +73,7 @@ def create_user(payload: UserCreate, db: Session = Depends(get_db), admin: User
|
||||||
phone=payload.phone,
|
phone=payload.phone,
|
||||||
name=payload.name,
|
name=payload.name,
|
||||||
department=payload.department,
|
department=payload.department,
|
||||||
|
job_title=payload.job_title,
|
||||||
unit_id=payload.unit_id,
|
unit_id=payload.unit_id,
|
||||||
is_superuser=payload.is_superuser,
|
is_superuser=payload.is_superuser,
|
||||||
)
|
)
|
||||||
|
|
@ -98,7 +100,7 @@ def update_user(
|
||||||
if "unit_id" in updates:
|
if "unit_id" in updates:
|
||||||
_validate_unit_id(db, updates["unit_id"])
|
_validate_unit_id(db, updates["unit_id"])
|
||||||
user.unit_id = updates["unit_id"]
|
user.unit_id = updates["unit_id"]
|
||||||
for field in ("name", "department", "status", "is_superuser"):
|
for field in ("name", "department", "job_title", "status", "is_superuser"):
|
||||||
if field in updates and updates[field] is not None:
|
if field in updates and updates[field] is not None:
|
||||||
setattr(user, field, updates[field])
|
setattr(user, field, updates[field])
|
||||||
if payload.role_codes is not None:
|
if payload.role_codes is not None:
|
||||||
|
|
@ -160,6 +162,7 @@ def bulk_import(
|
||||||
phone = (row.get("phone") or "").strip()
|
phone = (row.get("phone") or "").strip()
|
||||||
u_name = (row.get("name") or "").strip()
|
u_name = (row.get("name") or "").strip()
|
||||||
dept = (row.get("department") or "").strip() or None
|
dept = (row.get("department") or "").strip() or None
|
||||||
|
job_title = (row.get("job_title") or "").strip() or None
|
||||||
roles_raw = (row.get("role_codes") or "").strip()
|
roles_raw = (row.get("role_codes") or "").strip()
|
||||||
if not phone or not u_name:
|
if not phone or not u_name:
|
||||||
errors.append(f"第 {idx} 行:手机号或姓名为空")
|
errors.append(f"第 {idx} 行:手机号或姓名为空")
|
||||||
|
|
@ -168,7 +171,7 @@ def bulk_import(
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
role_codes = [c.strip() for c in roles_raw.replace(",", ",").split(",") if c.strip()]
|
role_codes = [c.strip() for c in roles_raw.replace(",", ",").split(",") if c.strip()]
|
||||||
user = User(phone=phone, name=u_name, department=dept)
|
user = User(phone=phone, name=u_name, department=dept, job_title=job_title)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.flush()
|
db.flush()
|
||||||
apply_roles(db, user, role_codes)
|
apply_roles(db, user, role_codes)
|
||||||
|
|
@ -187,6 +190,7 @@ def _parse_user_rows(raw: bytes, filename: str) -> list[dict]:
|
||||||
"手机号": "phone", "phone": "phone", "电话": "phone",
|
"手机号": "phone", "phone": "phone", "电话": "phone",
|
||||||
"姓名": "name", "name": "name", "用户名": "name", "用户名称": "name",
|
"姓名": "name", "name": "name", "用户名": "name", "用户名称": "name",
|
||||||
"部门": "department", "department": "department",
|
"部门": "department", "department": "department",
|
||||||
|
"职级": "job_title", "岗位": "job_title", "职位": "job_title", "job_title": "job_title",
|
||||||
"角色": "role_codes", "role_codes": "role_codes", "角色代码": "role_codes",
|
"角色": "role_codes", "role_codes": "role_codes", "角色代码": "role_codes",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ def get_db() -> Generator[Session, None, None]:
|
||||||
_LIGHTWEIGHT_ALTERS: list[tuple[str, str, str]] = [
|
_LIGHTWEIGHT_ALTERS: list[tuple[str, str, str]] = [
|
||||||
("attachments", "remote_url", "VARCHAR(1000)"),
|
("attachments", "remote_url", "VARCHAR(1000)"),
|
||||||
("users", "unit_id", "INTEGER"),
|
("users", "unit_id", "INTEGER"),
|
||||||
|
("users", "job_title", "VARCHAR(80)"),
|
||||||
("units", "parent_id", "INTEGER"),
|
("units", "parent_id", "INTEGER"),
|
||||||
("units", "category", "VARCHAR(20) DEFAULT 'company'"),
|
("units", "category", "VARCHAR(20) DEFAULT 'company'"),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
from app.models.audit import AuditLog, ApiCallLog, UsageDaily
|
from app.models.audit import AuditLog, ApiCallLog, UsageDaily
|
||||||
from app.models.chat import Attachment, Conversation, Feedback, Message, MessageReference
|
from app.models.chat import Attachment, Conversation, Feedback, Message, MessageReference
|
||||||
from app.models.document import Document, DocumentCategory, DocumentTag, document_tag_relations
|
from app.models.document import Document, DocumentCategory, DocumentTag, document_tag_relations
|
||||||
from app.models.user import Permission, Role, Unit, User, role_permissions, user_roles
|
from app.models.user import JobTitle, Permission, Role, Unit, User, role_permissions, user_roles
|
||||||
|
|
||||||
all_models = (
|
all_models = (
|
||||||
User,
|
User,
|
||||||
Role,
|
Role,
|
||||||
Permission,
|
Permission,
|
||||||
Unit,
|
Unit,
|
||||||
|
JobTitle,
|
||||||
DocumentCategory,
|
DocumentCategory,
|
||||||
Document,
|
Document,
|
||||||
DocumentTag,
|
DocumentTag,
|
||||||
|
|
@ -30,6 +31,7 @@ __all__ = [
|
||||||
"DocumentCategory",
|
"DocumentCategory",
|
||||||
"DocumentTag",
|
"DocumentTag",
|
||||||
"Feedback",
|
"Feedback",
|
||||||
|
"JobTitle",
|
||||||
"Message",
|
"Message",
|
||||||
"MessageReference",
|
"MessageReference",
|
||||||
"Permission",
|
"Permission",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, String, Table
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Table
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from app.db import Base
|
from app.db import Base
|
||||||
|
|
@ -28,6 +28,7 @@ class User(Base):
|
||||||
phone: Mapped[str] = mapped_column(String(32), unique=True, index=True)
|
phone: Mapped[str] = mapped_column(String(32), unique=True, index=True)
|
||||||
name: Mapped[str] = mapped_column(String(80))
|
name: Mapped[str] = mapped_column(String(80))
|
||||||
department: Mapped[str | None] = mapped_column(String(120))
|
department: Mapped[str | None] = mapped_column(String(120))
|
||||||
|
job_title: Mapped[str | None] = mapped_column(String(80))
|
||||||
unit_id: Mapped[int | None] = mapped_column(ForeignKey("units.id"), index=True)
|
unit_id: Mapped[int | None] = mapped_column(ForeignKey("units.id"), index=True)
|
||||||
status: Mapped[str] = mapped_column(String(20), default="active", index=True)
|
status: Mapped[str] = mapped_column(String(20), default="active", index=True)
|
||||||
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
|
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
@ -75,3 +76,16 @@ class Permission(Base):
|
||||||
module: Mapped[str] = mapped_column(String(60), index=True)
|
module: Mapped[str] = mapped_column(String(60), index=True)
|
||||||
|
|
||||||
roles: Mapped[list[Role]] = relationship(secondary=role_permissions, back_populates="permissions")
|
roles: Mapped[list[Role]] = relationship(secondary=role_permissions, back_populates="permissions")
|
||||||
|
|
||||||
|
|
||||||
|
class JobTitle(Base):
|
||||||
|
"""业务职级字典。User.job_title 存的是这里的 name(自由文本外键),
|
||||||
|
字典改名/删除不影响历史用户数据。"""
|
||||||
|
|
||||||
|
__tablename__ = "job_titles"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(80), unique=True, index=True)
|
||||||
|
sort_order: Mapped[int] = mapped_column(Integer, default=0, index=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class CurrentUserResponse(BaseModel):
|
||||||
phone: str
|
phone: str
|
||||||
name: str
|
name: str
|
||||||
department: str | None = None
|
department: str | None = None
|
||||||
|
job_title: str | None = None
|
||||||
unit_id: int | None = None
|
unit_id: int | None = None
|
||||||
unit_name: str | None = None
|
unit_name: str | None = None
|
||||||
status: str
|
status: str
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class JobTitleResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
sort_order: int
|
||||||
|
is_active: bool
|
||||||
|
|
@ -7,6 +7,7 @@ class UserCreate(BaseModel):
|
||||||
phone: str = Field(min_length=6, max_length=32)
|
phone: str = Field(min_length=6, max_length=32)
|
||||||
name: str = Field(min_length=1, max_length=80)
|
name: str = Field(min_length=1, max_length=80)
|
||||||
department: str | None = None
|
department: str | None = None
|
||||||
|
job_title: str | None = None
|
||||||
unit_id: int | None = None
|
unit_id: int | None = None
|
||||||
role_codes: list[str] = []
|
role_codes: list[str] = []
|
||||||
is_superuser: bool = False
|
is_superuser: bool = False
|
||||||
|
|
@ -15,6 +16,7 @@ class UserCreate(BaseModel):
|
||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
name: str | None = None
|
name: str | None = None
|
||||||
department: str | None = None
|
department: str | None = None
|
||||||
|
job_title: str | None = None
|
||||||
unit_id: int | None = None
|
unit_id: int | None = None
|
||||||
status: str | None = None
|
status: str | None = None
|
||||||
role_codes: list[str] | None = None
|
role_codes: list[str] | None = None
|
||||||
|
|
@ -26,6 +28,7 @@ class UserResponse(BaseModel):
|
||||||
phone: str
|
phone: str
|
||||||
name: str
|
name: str
|
||||||
department: str | None
|
department: str | None
|
||||||
|
job_title: str | None = None
|
||||||
unit_id: int | None = None
|
unit_id: int | None = None
|
||||||
unit_name: str | None = None
|
unit_name: str | None = None
|
||||||
status: str
|
status: str
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from datetime import datetime
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models import Permission, Role, User
|
from app.models import JobTitle, Permission, Role, User
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ROLES = {
|
DEFAULT_ROLES = {
|
||||||
|
|
@ -12,6 +12,37 @@ DEFAULT_ROLES = {
|
||||||
"system_admin": "系统管理员",
|
"system_admin": "系统管理员",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 业务职级预设。用户在个人资料里选的头衔。跟制度里的分组名不一定字面一致,
|
||||||
|
# 字面不一致时必须在 JOB_TITLE_GROUP_ALIASES 里加映射,否则 chat.py 拿 job_title
|
||||||
|
# 做子串匹配会全部落空,LLM 反问澄清。
|
||||||
|
DEFAULT_JOB_TITLES: list[str] = [
|
||||||
|
"董事长",
|
||||||
|
"总经理",
|
||||||
|
"领导班子副职",
|
||||||
|
"总助级",
|
||||||
|
"中层干部正职及相当职级人员",
|
||||||
|
"中层干部副职及相当职级人员",
|
||||||
|
"教授级高工及相当职称人员",
|
||||||
|
"普通员工",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 业务职级 -> 制度里"按职级分组"分组名的关键词列表。
|
||||||
|
# 制度里分组常写作"总院领导班子正职"这种复合词,用户选的却是"董事长"这种具体头衔,
|
||||||
|
# chat.py 会拿这里的关键词代替原 job_title 做子串规则("包含任一关键词")。
|
||||||
|
# 未在此表的 job_title 回退成拿它本身做子串匹配(老行为)。
|
||||||
|
# 加/改字典项时,同步维护此表;关键词要够特异,避免误命中相邻分组
|
||||||
|
# (例如"正职"太泛,会同时命中"领导班子正职"和"中层正职")。
|
||||||
|
JOB_TITLE_GROUP_ALIASES: dict[str, list[str]] = {
|
||||||
|
"董事长": ["领导班子正职"],
|
||||||
|
"总经理": ["领导班子正职"],
|
||||||
|
"领导班子副职": ["领导班子副职"],
|
||||||
|
"总助级": ["总助级"],
|
||||||
|
"中层干部正职及相当职级人员": ["中层正职", "中层干部正职"],
|
||||||
|
"中层干部副职及相当职级人员": ["中层副职", "中层干部副职"],
|
||||||
|
"教授级高工及相当职称人员": ["教授级高工"],
|
||||||
|
"普通员工": ["普通员工"],
|
||||||
|
}
|
||||||
|
|
||||||
# 页面级权限:code -> (显示名, 模块)
|
# 页面级权限:code -> (显示名, 模块)
|
||||||
DEFAULT_PAGE_PERMISSIONS: dict[str, tuple[str, str]] = {
|
DEFAULT_PAGE_PERMISSIONS: dict[str, tuple[str, str]] = {
|
||||||
"page.chat": ("智能问答", "page"),
|
"page.chat": ("智能问答", "page"),
|
||||||
|
|
@ -62,8 +93,19 @@ def ensure_roles(db: Session) -> None:
|
||||||
db.flush()
|
db.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_job_titles(db: Session) -> None:
|
||||||
|
"""把 DEFAULT_JOB_TITLES 里没入库的补进去;已存在的保留,不覆盖启用状态/顺序。"""
|
||||||
|
existing = {jt.name for jt in db.scalars(select(JobTitle)).all()}
|
||||||
|
for idx, name in enumerate(DEFAULT_JOB_TITLES):
|
||||||
|
if name in existing:
|
||||||
|
continue
|
||||||
|
db.add(JobTitle(name=name, sort_order=idx, is_active=True))
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
|
||||||
def ensure_default_admin(db: Session) -> User:
|
def ensure_default_admin(db: Session) -> User:
|
||||||
ensure_roles(db)
|
ensure_roles(db)
|
||||||
|
ensure_job_titles(db)
|
||||||
user = db.scalar(select(User).where(User.phone == "13800000000"))
|
user = db.scalar(select(User).where(User.phone == "13800000000"))
|
||||||
if user:
|
if user:
|
||||||
return user
|
return user
|
||||||
|
|
|
||||||
|
|
@ -13,27 +13,39 @@ const store = useAppStore()
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const roles = ref([])
|
const roles = ref([])
|
||||||
const units = ref([])
|
const units = ref([])
|
||||||
|
const jobTitles = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
|
|
||||||
const editing = ref(null)
|
const editing = ref(null)
|
||||||
const editForm = ref({ id: null, phone: '', name: '', department: '', unit_id: null, role_codes: [], is_superuser: false, status: 'active' })
|
const editForm = ref({ id: null, phone: '', name: '', department: '', job_title: '', unit_id: null, role_codes: [], is_superuser: false, status: 'active' })
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
|
|
||||||
const roleOptions = computed(() => roles.value.map((r) => ({ label: r.name, value: r.code })))
|
const roleOptions = computed(() => roles.value.map((r) => ({ label: r.name, value: r.code })))
|
||||||
const unitOptions = computed(() => units.value.map((u) => ({ label: u.name, value: u.id })))
|
const unitOptions = computed(() => units.value.map((u) => ({ label: u.name, value: u.id })))
|
||||||
|
// 字典里已有的项 + 当前编辑行里的历史值(如果不在字典里,避免下拉显示为空)
|
||||||
|
const jobTitleOptions = computed(() => {
|
||||||
|
const opts = jobTitles.value.map((jt) => ({ label: jt.name, value: jt.name }))
|
||||||
|
const cur = (editForm.value.job_title || '').trim()
|
||||||
|
if (cur && !opts.some((o) => o.value === cur)) {
|
||||||
|
opts.push({ label: `${cur}(历史值,不在字典中)`, value: cur })
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
})
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const [u, r, un] = await Promise.all([
|
const [u, r, un, jt] = await Promise.all([
|
||||||
request(`/api/users${keyword.value ? `?q=${encodeURIComponent(keyword.value)}` : ''}`),
|
request(`/api/users${keyword.value ? `?q=${encodeURIComponent(keyword.value)}` : ''}`),
|
||||||
request('/api/roles'),
|
request('/api/roles'),
|
||||||
request('/api/units'),
|
request('/api/units'),
|
||||||
|
request('/api/job-titles'),
|
||||||
])
|
])
|
||||||
users.value = u || []
|
users.value = u || []
|
||||||
roles.value = r || []
|
roles.value = r || []
|
||||||
units.value = un || []
|
units.value = un || []
|
||||||
|
jobTitles.value = jt || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notify.error(`加载失败:${err.message}`)
|
notify.error(`加载失败:${err.message}`)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -42,7 +54,7 @@ async function loadAll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAdd() {
|
function openAdd() {
|
||||||
editForm.value = { id: null, phone: '', name: '', department: '', unit_id: null, role_codes: [], is_superuser: false, status: 'active' }
|
editForm.value = { id: null, phone: '', name: '', department: '', job_title: '', unit_id: null, role_codes: [], is_superuser: false, status: 'active' }
|
||||||
editing.value = 'add'
|
editing.value = 'add'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +64,7 @@ function openEdit(row) {
|
||||||
phone: row.phone,
|
phone: row.phone,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
department: row.department || '',
|
department: row.department || '',
|
||||||
|
job_title: row.job_title || '',
|
||||||
unit_id: row.unit_id ?? null,
|
unit_id: row.unit_id ?? null,
|
||||||
role_codes: [...(row.role_codes || [])],
|
role_codes: [...(row.role_codes || [])],
|
||||||
is_superuser: !!row.is_superuser,
|
is_superuser: !!row.is_superuser,
|
||||||
|
|
@ -73,6 +86,7 @@ async function onSubmit() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
phone: form.phone, name: form.name,
|
phone: form.phone, name: form.name,
|
||||||
department: form.department || null,
|
department: form.department || null,
|
||||||
|
job_title: form.job_title || null,
|
||||||
unit_id: form.unit_id ?? null,
|
unit_id: form.unit_id ?? null,
|
||||||
role_codes: form.role_codes,
|
role_codes: form.role_codes,
|
||||||
is_superuser: form.is_superuser,
|
is_superuser: form.is_superuser,
|
||||||
|
|
@ -85,6 +99,7 @@ async function onSubmit() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
department: form.department || null,
|
department: form.department || null,
|
||||||
|
job_title: form.job_title || null,
|
||||||
unit_id: form.unit_id ?? null,
|
unit_id: form.unit_id ?? null,
|
||||||
role_codes: form.role_codes,
|
role_codes: form.role_codes,
|
||||||
is_superuser: form.is_superuser,
|
is_superuser: form.is_superuser,
|
||||||
|
|
@ -152,6 +167,7 @@ const columns = [
|
||||||
{ title: '用户名称', key: 'name' },
|
{ title: '用户名称', key: 'name' },
|
||||||
{ title: '账号 ID', key: 'phone' },
|
{ title: '账号 ID', key: 'phone' },
|
||||||
{ title: '单位', key: 'unit_name', render: (row) => row.unit_name || '—' },
|
{ title: '单位', key: 'unit_name', render: (row) => row.unit_name || '—' },
|
||||||
|
{ title: '职级', key: 'job_title', render: (row) => row.job_title || '—' },
|
||||||
{
|
{
|
||||||
title: '角色',
|
title: '角色',
|
||||||
key: 'role_codes',
|
key: 'role_codes',
|
||||||
|
|
@ -267,6 +283,16 @@ onMounted(loadAll)
|
||||||
<span>部门</span>
|
<span>部门</span>
|
||||||
<NInput v-model:value="editForm.department" placeholder="所在部门(可选)" />
|
<NInput v-model:value="editForm.department" placeholder="所在部门(可选)" />
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>职级</span>
|
||||||
|
<NSelect
|
||||||
|
v-model:value="editForm.job_title"
|
||||||
|
:options="jobTitleOptions"
|
||||||
|
placeholder="选择职级(用于问答时匹配制度条款)"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>角色</span>
|
<span>角色</span>
|
||||||
<NSelect
|
<NSelect
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue