feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,10 +18,12 @@ import logging
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.database import get_connection
|
||||
from app.trace.decorators import trace_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@trace_service(description_zh="创建入驻申请", description_en="Create application")
|
||||
async def create_application(
|
||||
user_id: int,
|
||||
site_code: str,
|
||||
@@ -60,11 +62,21 @@ async def create_application(
|
||||
detail="已有待审核的申请,请等待审核完成",
|
||||
)
|
||||
|
||||
# 2. 查找 site_code → site_id 映射
|
||||
# 2. 查找 site_code → site_id 映射(优先当前活跃编码,再查历史编码)
|
||||
# CHANGE 2026-03-23 | 大小写不敏感匹配 site_code(UPPER)
|
||||
site_id = None
|
||||
site_code_upper = site_code.upper()
|
||||
cur.execute(
|
||||
"SELECT site_id FROM auth.site_code_mapping WHERE site_code = %s",
|
||||
(site_code,),
|
||||
"""
|
||||
SELECT site_id FROM biz.sites
|
||||
WHERE UPPER(site_code) = %s AND is_active = true
|
||||
UNION ALL
|
||||
SELECT s.site_id FROM biz.site_code_history h
|
||||
JOIN biz.sites s ON s.site_id = h.site_id
|
||||
WHERE UPPER(h.site_code) = %s AND h.is_current = false AND s.is_active = true
|
||||
LIMIT 1
|
||||
""",
|
||||
(site_code_upper, site_code_upper),
|
||||
)
|
||||
mapping_row = cur.fetchone()
|
||||
if mapping_row is not None:
|
||||
@@ -123,6 +135,7 @@ async def create_application(
|
||||
|
||||
|
||||
|
||||
@trace_service(description_zh="审批通过申请", description_en="Approve application")
|
||||
async def approve_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
@@ -248,6 +261,7 @@ async def approve_application(
|
||||
}
|
||||
|
||||
|
||||
@trace_service(description_zh="驳回申请", description_en="Reject application")
|
||||
async def reject_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
@@ -260,16 +274,18 @@ async def reject_application(
|
||||
2. 检查申请状态为 pending(否则 409)
|
||||
3. 更新 user_applications.status = 'rejected'
|
||||
4. 记录 reviewer_id、review_note、reviewed_at
|
||||
5. 累加 users.rejection_count,达到 3 次自动禁用
|
||||
|
||||
返回:
|
||||
更新后的申请记录 dict
|
||||
更新后的申请记录 dict(含 user_disabled 标记)
|
||||
"""
|
||||
conn = get_connection()
|
||||
user_disabled = False
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 1. 查询申请记录
|
||||
# 1. 查询申请记录(含 user_id)
|
||||
cur.execute(
|
||||
"SELECT id, status FROM auth.user_applications WHERE id = %s",
|
||||
"SELECT id, user_id, status FROM auth.user_applications WHERE id = %s",
|
||||
(application_id,),
|
||||
)
|
||||
app_row = cur.fetchone()
|
||||
@@ -279,11 +295,13 @@ async def reject_application(
|
||||
detail="申请不存在",
|
||||
)
|
||||
|
||||
_, app_user_id, app_status = app_row
|
||||
|
||||
# 2. 检查状态为 pending
|
||||
if app_row[1] != "pending":
|
||||
if app_status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"申请当前状态为 {app_row[1]},无法审核",
|
||||
detail=f"申请当前状态为 {app_status},无法审核",
|
||||
)
|
||||
|
||||
# 3. 更新申请状态为 rejected
|
||||
@@ -301,6 +319,46 @@ async def reject_application(
|
||||
(reviewer_id, review_note, application_id),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
|
||||
# 4. 累加 rejection_count 并检查是否达到禁用阈值
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.users
|
||||
SET rejection_count = rejection_count + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING rejection_count
|
||||
""",
|
||||
(app_user_id,),
|
||||
)
|
||||
new_count = cur.fetchone()[0]
|
||||
|
||||
if new_count >= 3:
|
||||
# 第三次拒绝:自动禁用账号
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.users
|
||||
SET status = 'disabled', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(app_user_id,),
|
||||
)
|
||||
user_disabled = True
|
||||
logger.warning(
|
||||
"用户 %s 累计被拒绝 %d 次,已自动禁用",
|
||||
app_user_id, new_count,
|
||||
)
|
||||
else:
|
||||
# 未达阈值:回退用户状态为 rejected(允许重新申请)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.users
|
||||
SET status = 'rejected', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(app_user_id,),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -313,9 +371,11 @@ async def reject_application(
|
||||
"review_note": updated_row[4],
|
||||
"created_at": updated_row[5],
|
||||
"reviewed_at": updated_row[6],
|
||||
"user_disabled": user_disabled,
|
||||
}
|
||||
|
||||
|
||||
@trace_service(description_zh="获取用户申请列表", description_en="Get user applications")
|
||||
async def get_user_applications(user_id: int) -> list[dict]:
|
||||
"""
|
||||
查询用户的所有申请记录。
|
||||
@@ -323,14 +383,15 @@ async def get_user_applications(user_id: int) -> list[dict]:
|
||||
按创建时间倒序排列。
|
||||
|
||||
返回:
|
||||
申请记录 dict 列表
|
||||
申请记录 dict 列表(含 phone、employee_number)
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, site_code, applied_role_text, status,
|
||||
SELECT id, site_code, applied_role_text, phone,
|
||||
employee_number, status,
|
||||
review_note, created_at::text, reviewed_at::text
|
||||
FROM auth.user_applications
|
||||
WHERE user_id = %s
|
||||
@@ -347,10 +408,84 @@ async def get_user_applications(user_id: int) -> list[dict]:
|
||||
"id": r[0],
|
||||
"site_code": r[1],
|
||||
"applied_role_text": r[2],
|
||||
"status": r[3],
|
||||
"review_note": r[4],
|
||||
"created_at": r[5],
|
||||
"reviewed_at": r[6],
|
||||
"phone": r[3],
|
||||
"employee_number": r[4],
|
||||
"status": r[5],
|
||||
"review_note": r[6],
|
||||
"created_at": r[7],
|
||||
"reviewed_at": r[8],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@trace_service(description_zh="取消申请", description_en="Cancel application")
|
||||
async def cancel_application(user_id: int) -> dict:
|
||||
"""
|
||||
用户主动取消当前 pending 申请。
|
||||
|
||||
1. 查找用户的 pending 申请(无则 404)
|
||||
2. 更新申请 status = 'cancelled'
|
||||
3. 回退用户 status 为 'new'
|
||||
|
||||
返回:
|
||||
被取消的申请记录 dict
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 1. 查找 pending 申请
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id FROM auth.user_applications
|
||||
WHERE user_id = %s AND status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="没有待审核的申请",
|
||||
)
|
||||
|
||||
application_id = row[0]
|
||||
|
||||
# 2. 更新申请状态为 cancelled
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.user_applications
|
||||
SET status = 'cancelled'
|
||||
WHERE id = %s
|
||||
RETURNING id, site_code, applied_role_text, phone,
|
||||
employee_number, status, created_at::text
|
||||
""",
|
||||
(application_id,),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
|
||||
# 3. 回退用户状态为 new
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.users
|
||||
SET status = 'new', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"id": updated_row[0],
|
||||
"site_code": updated_row[1],
|
||||
"applied_role_text": updated_row[2],
|
||||
"phone": updated_row[3],
|
||||
"employee_number": updated_row[4],
|
||||
"status": updated_row[5],
|
||||
"created_at": updated_row[6],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user