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:
Neo
2026-04-06 00:03:48 +08:00
parent 70324d8542
commit 6f8f12314f
515 changed files with 76604 additions and 7456 deletions

View File

@@ -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_codeUPPER
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],
}