feat(stocktake): 活跃会话接口补充已扫件数与发起人
多人协同场景下,加入者需要知道「谁开的、盘到哪了」,原接口只返回会话ID和
行数,信息不足以提示。
- /draft/active-session 新增 scanned(按 source_table+stock_id 去重,
且排除 user_id='system' 的自动漏盘记录,避免进度虚高)
与 initiator(该会话最早一条人工扫码记录的操作人姓名)
- 把 export_stocktake 内嵌的 get_user_name 提到模块级 _resolve_user_name
供两处复用,并补齐它漏掉的 user_id 格式:
盘点/流水里实际存的是 JWT 的 display_name,形如 "孙霞(sunxia)",
而原实现只认 SysUser.username 的 "姓名/账号" 格式,导致解析不出来时
原样返回。现兼容 "姓名(账号)" / "姓名/账号" / 纯数字ID 三种。
实测: SUPER_ADMIN 无参数 → scanned=1117, initiator='韩善龙'
SALES + IRIS → 会话 company_name 为 NULL(迁移前遗留),正确返回空
This commit is contained in:
@ -50,6 +50,52 @@ def _normalize_user_id(user_id=None):
|
|||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_user_name(user_id):
|
||||||
|
"""把 user_id 解析为真实姓名,解析不出来就原样返回。
|
||||||
|
|
||||||
|
需要兼容三种来源:
|
||||||
|
1. "孙霞(sunxia)" —— 草稿/流水里实际存的格式,来自 JWT 的 display_name
|
||||||
|
2. "张三/zhangsan01" —— SysUser.username 的存储格式
|
||||||
|
3. 纯数字用户ID —— 直接按主键查
|
||||||
|
"""
|
||||||
|
if not user_id:
|
||||||
|
return '-'
|
||||||
|
raw = str(user_id).strip()
|
||||||
|
|
||||||
|
# 1. "姓名(账号)" / "姓名/账号" —— 分隔符前就是姓名,无需查库
|
||||||
|
if '(' in raw and raw.endswith(')'):
|
||||||
|
name = raw.split('(', 1)[0].strip()
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
if '/' in raw:
|
||||||
|
name = raw.split('/', 1)[0].strip()
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
|
||||||
|
if not SysUser:
|
||||||
|
return raw
|
||||||
|
try:
|
||||||
|
user = None
|
||||||
|
# 尝试通过ID或用户名查找
|
||||||
|
if raw.isdigit():
|
||||||
|
user = SysUser.query.get(int(raw))
|
||||||
|
if not user:
|
||||||
|
user = SysUser.query.filter(SysUser.username.like(f"%/{raw}")).first()
|
||||||
|
# 注意:此处不做 filter_by(username=...) 兜底,
|
||||||
|
# 避免 PostgreSQL 把 user_id 数字与 username 字符串列做类型比较导致报错
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
# 2. 解析 username 格式: "张三/zhangsan01" -> 取前面的真实姓名
|
||||||
|
raw_username = getattr(user, 'username', None) or raw
|
||||||
|
if '/' in raw_username:
|
||||||
|
return raw_username.split('/')[0]
|
||||||
|
return raw_username
|
||||||
|
except Exception:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def get_stock_model(source_table):
|
def get_stock_model(source_table):
|
||||||
"""根据source_table获取对应的库存模型"""
|
"""根据source_table获取对应的库存模型"""
|
||||||
if source_table == 'stock_buy':
|
if source_table == 'stock_buy':
|
||||||
@ -790,7 +836,13 @@ def get_active_session():
|
|||||||
「活跃」的定义:该公司所有草稿中 scan_time 最大的那个 session_id。
|
「活跃」的定义:该公司所有草稿中 scan_time 最大的那个 session_id。
|
||||||
注意 /draft/list 是按 SKU 排序的,拿不到「最近会话」,故单列此接口。
|
注意 /draft/list 是按 SKU 排序的,拿不到「最近会话」,故单列此接口。
|
||||||
|
|
||||||
返回: { session_id: str|null, total: int, last_scan_time: str|null }
|
返回: {
|
||||||
|
session_id: str|null,
|
||||||
|
total: int, # 该会话的草稿行数
|
||||||
|
scanned: int, # 实际扫到的件数(按物料去重,排除系统自动生成的漏盘记录)
|
||||||
|
last_scan_time: str|null,
|
||||||
|
initiator: str|null, # 发起人显示名(该会话最早一条扫码记录的操作人)
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
company_name = get_current_company_filter()
|
company_name = get_current_company_filter()
|
||||||
|
|
||||||
@ -811,15 +863,45 @@ def get_active_session():
|
|||||||
if not row or not row.session_id:
|
if not row or not row.session_id:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'data': {'session_id': None, 'total': 0, 'last_scan_time': None}
|
'data': {
|
||||||
|
'session_id': None, 'total': 0, 'scanned': 0,
|
||||||
|
'last_scan_time': None, 'initiator': None
|
||||||
|
}
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
session_id = row.session_id
|
||||||
|
|
||||||
|
# 该会话的明细统计 —— 必须同样带上公司条件,避免跨公司串号
|
||||||
|
def _scoped(q):
|
||||||
|
if company_name is not None:
|
||||||
|
q = q.filter(StocktakeDraft.company_name == company_name)
|
||||||
|
return q.filter(StocktakeDraft.session_id == session_id)
|
||||||
|
|
||||||
|
# 已扫件数:按 (source_table, stock_id) 去重。
|
||||||
|
# 排除 user_id == 'system' 的记录 —— 那是「结束盘点」时 generate-missing
|
||||||
|
# 自动生成的漏盘(实盘=0),不是人扫到的,计入会让进度虚高。
|
||||||
|
scanned = _scoped(
|
||||||
|
db.session.query(StocktakeDraft.source_table, StocktakeDraft.stock_id)
|
||||||
|
.filter(StocktakeDraft.user_id != 'system')
|
||||||
|
).distinct().count()
|
||||||
|
|
||||||
|
# 发起人:该会话最早一条非系统扫码记录的操作人。
|
||||||
|
# 注:/draft/add 更新已有记录时会把 user_id 改写成「最后操作者」,
|
||||||
|
# 所以这里取到的是最早那条记录的最后操作人,是当前模型下最接近发起人的信号。
|
||||||
|
first_row = _scoped(
|
||||||
|
db.session.query(StocktakeDraft.user_id)
|
||||||
|
.filter(StocktakeDraft.user_id != 'system')
|
||||||
|
).order_by(StocktakeDraft.scan_time.asc()).first()
|
||||||
|
initiator = _resolve_user_name(first_row[0]) if first_row and first_row[0] else None
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'data': {
|
'data': {
|
||||||
'session_id': row.session_id,
|
'session_id': session_id,
|
||||||
'total': int(row.total or 0),
|
'total': int(row.total or 0),
|
||||||
'last_scan_time': row.last_scan_time.strftime('%Y-%m-%d %H:%M:%S') if row.last_scan_time else None
|
'scanned': int(scanned or 0),
|
||||||
|
'last_scan_time': row.last_scan_time.strftime('%Y-%m-%d %H:%M:%S') if row.last_scan_time else None,
|
||||||
|
'initiator': initiator
|
||||||
}
|
}
|
||||||
}), 200
|
}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1165,31 +1247,7 @@ def export_stocktake():
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_user_name(user_id):
|
def get_user_name(user_id):
|
||||||
"""获取用户真实姓名
|
return _resolve_user_name(user_id)
|
||||||
SysUser.username 存储格式为 "真实姓名/登录账号" (例如: 张三/zhangsan01)
|
|
||||||
"""
|
|
||||||
if not SysUser or not user_id:
|
|
||||||
return str(user_id) if user_id else '-'
|
|
||||||
try:
|
|
||||||
user = None
|
|
||||||
# 尝试通过ID或用户名查找
|
|
||||||
if str(user_id).isdigit():
|
|
||||||
user = SysUser.query.get(int(user_id))
|
|
||||||
if not user:
|
|
||||||
user = SysUser.query.filter(SysUser.username.like(f"%/{user_id}")).first()
|
|
||||||
# 注意:此处不再 fallback filter_by(username=...),
|
|
||||||
# 避免 PostgreSQL 将 user_id 数字与 username 字符串列做类型比较导致报错
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return str(user_id)
|
|
||||||
|
|
||||||
# 解析 username 格式: "张三/zhangsan01" -> 取前面的真实姓名
|
|
||||||
raw_username = getattr(user, 'username', None) or str(user_id)
|
|
||||||
if '/' in raw_username:
|
|
||||||
return raw_username.split('/')[0]
|
|
||||||
return raw_username
|
|
||||||
except:
|
|
||||||
return str(user_id)
|
|
||||||
|
|
||||||
def to_beijing_time(dt):
|
def to_beijing_time(dt):
|
||||||
"""直接使用数据库中存储的标准时间(服务器时区已正确)"""
|
"""直接使用数据库中存储的标准时间(服务器时区已正确)"""
|
||||||
|
|||||||
Reference in New Issue
Block a user