feat: P1-P3 全栈集成 — 数据库基础 + DWS 扩展 + 小程序鉴权 + 工程化体系
## P1 数据库基础 - zqyy_app: 创建 auth/biz schema、FDW 连接 etl_feiqiu - etl_feiqiu: 创建 app schema RLS 视图、商品库存预警表 - 清理 assistant_abolish 残留数据 ## P2 ETL/DWS 扩展 - 新增 DWS 助教订单贡献度表 (dws.assistant_order_contribution) - 新增 assistant_order_contribution_task 任务及 RLS 视图 - member_consumption 增加充值字段、assistant_daily 增加处罚字段 - 更新 ODS/DWD/DWS 任务文档及业务规则文档 - 更新 consistency_checker、flow_runner、task_registry 等核心模块 ## P3 小程序鉴权系统 - 新增 xcx_auth 路由/schema(微信登录 + JWT) - 新增 wechat/role/matching/application 服务层 - zqyy_app 鉴权表迁移 + 角色权限种子数据 - auth/dependencies.py 支持小程序 JWT 鉴权 ## 文档与审计 - 新增 DOCUMENTATION-MAP 文档导航 - 新增 7 份 BD_Manual 数据库变更文档 - 更新 DDL 基线快照(etl_feiqiu 6 schema + zqyy_app auth) - 新增全栈集成审计记录、部署检查清单更新 - 新增 BACKLOG 路线图、FDW→Core 迁移计划 ## Kiro 工程化 - 新增 5 个 Spec(P1/P2/P3/全栈集成/核心业务) - 新增审计自动化脚本(agent_on_stop/build_audit_context/compliance_prescan) - 新增 6 个 Hook(合规检查/会话日志/提交审计等) - 新增 doc-map steering 文件 ## 运维与测试 - 新增 ops 脚本:迁移验证/API 健康检查/ETL 监控/集成报告 - 新增属性测试:test_dws_contribution / test_auth_system - 清理过期 export 报告文件 - 更新 .gitignore 排除规则
This commit is contained in:
347
apps/backend/app/services/application.py
Normal file
347
apps/backend/app/services/application.py
Normal file
@@ -0,0 +1,347 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
申请服务 —— 处理用户申请的创建、查询、审核。
|
||||
|
||||
职责:
|
||||
- create_application():创建申请 + site_code 映射查找
|
||||
- approve_application():批准 + 创建绑定/角色
|
||||
- reject_application():拒绝 + 记录原因
|
||||
- get_user_applications():查询用户申请列表
|
||||
|
||||
所有数据库操作使用 psycopg2 原生 SQL,不引入 ORM。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from app.database import get_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_application(
|
||||
user_id: int,
|
||||
site_code: str,
|
||||
applied_role_text: str,
|
||||
phone: str,
|
||||
employee_number: str | None = None,
|
||||
nickname: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
创建用户申请。
|
||||
|
||||
1. 检查是否有 pending 申请(有则 409)
|
||||
2. 查找 site_code → site_id 映射
|
||||
3. 插入 user_applications 记录
|
||||
4. 更新 users.nickname(如提供)
|
||||
|
||||
返回:
|
||||
申请记录 dict,包含 id / site_code / applied_role_text / status /
|
||||
review_note / created_at / reviewed_at
|
||||
"""
|
||||
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'
|
||||
LIMIT 1
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
if cur.fetchone() is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="已有待审核的申请,请等待审核完成",
|
||||
)
|
||||
|
||||
# 2. 查找 site_code → site_id 映射
|
||||
site_id = None
|
||||
cur.execute(
|
||||
"SELECT site_id FROM auth.site_code_mapping WHERE site_code = %s",
|
||||
(site_code,),
|
||||
)
|
||||
mapping_row = cur.fetchone()
|
||||
if mapping_row is not None:
|
||||
site_id = mapping_row[0]
|
||||
|
||||
# 3. 创建申请记录
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO auth.user_applications
|
||||
(user_id, site_code, site_id, applied_role_text,
|
||||
phone, employee_number, status)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, 'pending')
|
||||
RETURNING id, site_code, applied_role_text, status,
|
||||
review_note, created_at::text, reviewed_at::text
|
||||
""",
|
||||
(
|
||||
user_id,
|
||||
site_code,
|
||||
site_id,
|
||||
applied_role_text,
|
||||
phone,
|
||||
employee_number,
|
||||
),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
# 4. 更新 nickname(如提供)
|
||||
if nickname:
|
||||
cur.execute(
|
||||
"UPDATE auth.users SET nickname = %s, updated_at = NOW() WHERE id = %s",
|
||||
(nickname, user_id),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"id": row[0],
|
||||
"site_code": row[1],
|
||||
"applied_role_text": row[2],
|
||||
"status": row[3],
|
||||
"review_note": row[4],
|
||||
"created_at": row[5],
|
||||
"reviewed_at": row[6],
|
||||
}
|
||||
|
||||
|
||||
|
||||
async def approve_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
role_id: int,
|
||||
binding: dict | None = None,
|
||||
review_note: str | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
批准申请。
|
||||
|
||||
1. 查询申请记录(不存在则 404)
|
||||
2. 检查申请状态为 pending(否则 409)
|
||||
3. 更新 user_applications.status = 'approved'
|
||||
4. 创建 user_site_roles 记录
|
||||
5. 创建 user_assistant_binding 记录(如有 binding)
|
||||
6. 更新 users.status = 'approved'
|
||||
7. 记录 reviewer_id 和 reviewed_at
|
||||
|
||||
参数:
|
||||
application_id: 申请 ID
|
||||
reviewer_id: 审核人 user_id
|
||||
role_id: 分配的角色 ID
|
||||
binding: 绑定信息,格式 {"assistant_id": ..., "staff_id": ..., "binding_type": ...}
|
||||
review_note: 审核备注(可选)
|
||||
|
||||
返回:
|
||||
更新后的申请记录 dict
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 1. 查询申请记录
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, user_id, site_id, status
|
||||
FROM auth.user_applications
|
||||
WHERE id = %s
|
||||
""",
|
||||
(application_id,),
|
||||
)
|
||||
app_row = cur.fetchone()
|
||||
if app_row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="申请不存在",
|
||||
)
|
||||
|
||||
_, app_user_id, app_site_id, app_status = app_row
|
||||
|
||||
# 2. 检查状态为 pending
|
||||
if app_status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"申请当前状态为 {app_status},无法审核",
|
||||
)
|
||||
|
||||
# 3. 更新申请状态为 approved
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.user_applications
|
||||
SET status = 'approved',
|
||||
reviewer_id = %s,
|
||||
review_note = %s,
|
||||
reviewed_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, site_code, applied_role_text, status,
|
||||
review_note, created_at::text, reviewed_at::text
|
||||
""",
|
||||
(reviewer_id, review_note, application_id),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
|
||||
# 4. 创建 user_site_roles 记录(如果有 site_id)
|
||||
if app_site_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO auth.user_site_roles (user_id, site_id, role_id)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (user_id, site_id, role_id) DO NOTHING
|
||||
""",
|
||||
(app_user_id, app_site_id, role_id),
|
||||
)
|
||||
|
||||
# 5. 创建 user_assistant_binding 记录(如有 binding 且有 site_id)
|
||||
if binding and app_site_id is not None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO auth.user_assistant_binding
|
||||
(user_id, site_id, assistant_id, staff_id, binding_type)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
app_user_id,
|
||||
app_site_id,
|
||||
binding.get("assistant_id"),
|
||||
binding.get("staff_id"),
|
||||
binding.get("binding_type", "assistant"),
|
||||
),
|
||||
)
|
||||
|
||||
# 6. 更新用户状态为 approved
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.users
|
||||
SET status = 'approved', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(app_user_id,),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"id": updated_row[0],
|
||||
"site_code": updated_row[1],
|
||||
"applied_role_text": updated_row[2],
|
||||
"status": updated_row[3],
|
||||
"review_note": updated_row[4],
|
||||
"created_at": updated_row[5],
|
||||
"reviewed_at": updated_row[6],
|
||||
}
|
||||
|
||||
|
||||
async def reject_application(
|
||||
application_id: int,
|
||||
reviewer_id: int,
|
||||
review_note: str,
|
||||
) -> dict:
|
||||
"""
|
||||
拒绝申请。
|
||||
|
||||
1. 查询申请记录(不存在则 404)
|
||||
2. 检查申请状态为 pending(否则 409)
|
||||
3. 更新 user_applications.status = 'rejected'
|
||||
4. 记录 reviewer_id、review_note、reviewed_at
|
||||
|
||||
返回:
|
||||
更新后的申请记录 dict
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
# 1. 查询申请记录
|
||||
cur.execute(
|
||||
"SELECT id, status FROM auth.user_applications WHERE id = %s",
|
||||
(application_id,),
|
||||
)
|
||||
app_row = cur.fetchone()
|
||||
if app_row is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="申请不存在",
|
||||
)
|
||||
|
||||
# 2. 检查状态为 pending
|
||||
if app_row[1] != "pending":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"申请当前状态为 {app_row[1]},无法审核",
|
||||
)
|
||||
|
||||
# 3. 更新申请状态为 rejected
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE auth.user_applications
|
||||
SET status = 'rejected',
|
||||
reviewer_id = %s,
|
||||
review_note = %s,
|
||||
reviewed_at = NOW()
|
||||
WHERE id = %s
|
||||
RETURNING id, site_code, applied_role_text, status,
|
||||
review_note, created_at::text, reviewed_at::text
|
||||
""",
|
||||
(reviewer_id, review_note, application_id),
|
||||
)
|
||||
updated_row = cur.fetchone()
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
"id": updated_row[0],
|
||||
"site_code": updated_row[1],
|
||||
"applied_role_text": updated_row[2],
|
||||
"status": updated_row[3],
|
||||
"review_note": updated_row[4],
|
||||
"created_at": updated_row[5],
|
||||
"reviewed_at": updated_row[6],
|
||||
}
|
||||
|
||||
|
||||
async def get_user_applications(user_id: int) -> list[dict]:
|
||||
"""
|
||||
查询用户的所有申请记录。
|
||||
|
||||
按创建时间倒序排列。
|
||||
|
||||
返回:
|
||||
申请记录 dict 列表
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, site_code, applied_role_text, status,
|
||||
review_note, created_at::text, reviewed_at::text
|
||||
FROM auth.user_applications
|
||||
WHERE user_id = %s
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
(user_id,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
{
|
||||
"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],
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
Reference in New Issue
Block a user