commit
6bc1a2c9f5
8
b/account/open_all_provider_acc.dspy
Normal file
8
b/account/open_all_provider_acc.dspy
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
async def open_all_provider_acc(ns):
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
accounting_orgid = ns.get('accounting_orgid')
|
||||||
|
await openAllProviderAccounts(sor, accounting_orgid)
|
||||||
|
return {'code':200}
|
||||||
|
ret = await open_all_provider_acc(params_kw)
|
||||||
|
return ret
|
||||||
@ -26,9 +26,12 @@ async def jiajie_sync_user(ns={}):
|
|||||||
}
|
}
|
||||||
userinfos = await sor.R('users', {'id': userid})
|
userinfos = await sor.R('users', {'id': userid})
|
||||||
phone = userinfos[0]['mobile']
|
phone = userinfos[0]['mobile']
|
||||||
email = userinfos[0]['email']
|
# email = userinfos[0]['email']
|
||||||
name = userinfos[0]['username']
|
name = userinfos[0]['username']
|
||||||
|
|
||||||
|
# 生成随机邮箱
|
||||||
|
email = f"{userid}@opencomputing.cn"
|
||||||
|
|
||||||
# 构造请求数据 筛选条件: 手机号/邮箱
|
# 构造请求数据 筛选条件: 手机号/邮箱
|
||||||
request_data = {
|
request_data = {
|
||||||
"password": "weishijiajiekyy",
|
"password": "weishijiajiekyy",
|
||||||
|
|||||||
1238
b/bill/finance_order_report.dspy
Normal file
1238
b/bill/finance_order_report.dspy
Normal file
File diff suppressed because it is too large
Load Diff
887
b/bill/finance_order_report1.dspy
Normal file
887
b/bill/finance_order_report1.dspy
Normal file
@ -0,0 +1,887 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
管理人员财务订单报表接口。
|
||||||
|
|
||||||
|
视角:以 accounting_orgid(账本机构)为查看主体,展示客户订单的定价、利润、本级应付结算。
|
||||||
|
|
||||||
|
依赖运行环境:DBPools, sor.sqlExe, sor.R
|
||||||
|
|
||||||
|
业务口径(已确认):
|
||||||
|
- product_salemode.providerid = provider.orgid
|
||||||
|
- 分销商不直接对供应商结算;仅本机构(parentid 为空)对供应商结算
|
||||||
|
- 一级分销对本机构结算,二级分销对一级分销结算(即对直接上级结算)
|
||||||
|
- settle_upstream_amount:本级应付(本机构→供应商;分销→直接上级机构)
|
||||||
|
- profit_amount:本级账本「折扣收入」「底价收入」贷方合计
|
||||||
|
- include_sub_reseller_customers:是否包含下级分销商发展的客户
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_parent_orgid(sor, orgid):
|
||||||
|
sql = """select a.id from organization a, organization b
|
||||||
|
where b.parentid = a.id
|
||||||
|
and a.del_flg = '0'
|
||||||
|
and b.del_flg = '0'
|
||||||
|
and b.id = ${orgid}$"""
|
||||||
|
recs = await sor.sqlExe(sql, {'orgid':orgid})
|
||||||
|
if len(recs) == 0:
|
||||||
|
return None
|
||||||
|
return recs[0]['id']
|
||||||
|
|
||||||
|
DBNAME = 'kboss'
|
||||||
|
RESELLER_ORG = '1'
|
||||||
|
OWNER_OGR = '0'
|
||||||
|
CORP_CUSTOMER = '2'
|
||||||
|
PERSONAL = '3'
|
||||||
|
PROVIDER = '4'
|
||||||
|
PUBLISHER = '5'
|
||||||
|
UNDERWRITER = '6'
|
||||||
|
|
||||||
|
PARTY_OWNER = '本机构'
|
||||||
|
PARTY_CUSTOMER = '客户'
|
||||||
|
PARTY_RESELLER = '分销商'
|
||||||
|
PARTY_PROVIDER = '供应商'
|
||||||
|
PARTY_PUBLISHER = '算力券发行方'
|
||||||
|
PARTY_UNDERWRITER = '算力券承销方'
|
||||||
|
|
||||||
|
DEBT = '借'
|
||||||
|
CREDIT = '贷'
|
||||||
|
|
||||||
|
ACTNAME_BUY = '付费'
|
||||||
|
ACTNAME_RECHARGE = '充值'
|
||||||
|
ACTNAME_RECHARGE_ALIPAY = '支付宝充值'
|
||||||
|
ACTNAME_SETTLE = '结算'
|
||||||
|
|
||||||
|
SALEMODE_DISCOUNT = '折扣'
|
||||||
|
SALEMODE_REBATE = '代付费'
|
||||||
|
SALEMODE_FLOORPRICE = '底价'
|
||||||
|
|
||||||
|
ACTION_RECHARGE_ALIPAY = 'RECHARGE_ALIPAY'
|
||||||
|
ACTION_RECHARGE_ALIPAY_REVERSE = 'RECHARGE_ALIPAY_REVERSE'
|
||||||
|
ACTION_RECHARGE = 'RECHARGE'
|
||||||
|
ACTION_RECHARGE_REVERSE = 'RECHARGE_REVERSE'
|
||||||
|
ACTION_BUY = 'BUY'
|
||||||
|
ACTION_REVERSE_BUY = 'BUY_REVERSE'
|
||||||
|
ACTION_RENEW = 'RENEW'
|
||||||
|
ACTION_RENEW_REVERSE = 'RENEW_REVERSE'
|
||||||
|
ACTION_SETTLE = 'SETTLE'
|
||||||
|
ACTION_SETTLE_REVERSE = 'SETTLE_REVERSE'
|
||||||
|
|
||||||
|
DBNAME = 'kboss'
|
||||||
|
CUSTOMER_ORG_TYPES = ('2', '3')
|
||||||
|
|
||||||
|
SALEMODE_LABEL = {
|
||||||
|
'0': SALEMODE_DISCOUNT,
|
||||||
|
'1': SALEMODE_REBATE,
|
||||||
|
'2': SALEMODE_FLOORPRICE,
|
||||||
|
}
|
||||||
|
|
||||||
|
INCOME_SUBJECTS = ('折扣收入', '底价收入')
|
||||||
|
PARENT_SETTLE_SUBJECT = '分销商存放资金'
|
||||||
|
SUPPLIER_SETTLE_PREFIX = '待结转'
|
||||||
|
|
||||||
|
_SALEMODE_SQL_OWN = """
|
||||||
|
SELECT a.salemode, a.settle_mode, b.discount, b.price
|
||||||
|
FROM saleprotocol a, product_salemode b
|
||||||
|
WHERE a.id = b.protocolid
|
||||||
|
AND a.bid_orgid = ${bid_orgid}$
|
||||||
|
AND (b.productid = ${productid}$ OR b.productid = '*')
|
||||||
|
AND b.providerid = ${providerid}$
|
||||||
|
AND a.start_date <= ${curdate}$
|
||||||
|
AND a.end_date > ${curdate}$
|
||||||
|
AND a.del_flg = '0'
|
||||||
|
AND b.del_flg = '0'
|
||||||
|
ORDER BY b.productid DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SALEMODE_SQL_OFFER = """
|
||||||
|
SELECT a.salemode, a.settle_mode, b.discount, b.price
|
||||||
|
FROM saleprotocol a, product_salemode b
|
||||||
|
WHERE a.id = b.protocolid
|
||||||
|
AND a.offer_orgid = ${offer_orgid}$
|
||||||
|
AND a.bid_orgid = ${bid_orgid}$
|
||||||
|
AND (b.productid = ${productid}$ OR b.productid = '*')
|
||||||
|
AND b.providerid = ${providerid}$
|
||||||
|
AND a.start_date <= ${curdate}$
|
||||||
|
AND a.end_date > ${curdate}$
|
||||||
|
AND a.del_flg = '0'
|
||||||
|
AND b.del_flg = '0'
|
||||||
|
ORDER BY b.productid DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _round_money(v):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
return round(float(v), 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _salemode_label(code):
|
||||||
|
if code is None:
|
||||||
|
return None
|
||||||
|
return SALEMODE_LABEL.get(str(code), str(code))
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_bool(v, default=False):
|
||||||
|
if v is None:
|
||||||
|
return default
|
||||||
|
if isinstance(v, bool):
|
||||||
|
return v
|
||||||
|
return str(v).strip().lower() in ('1', 'true', 'yes', 'on')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_page(ns, default_page=1, default_size=20, max_size=100):
|
||||||
|
"""解析分页,避免 page_size=0 导致 LIMIT 0 无数据。"""
|
||||||
|
try:
|
||||||
|
page_size = int(ns.get('page_size', default_size) or default_size)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
page_size = default_size
|
||||||
|
try:
|
||||||
|
current_page = int(ns.get('current_page', default_page) or default_page)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
current_page = default_page
|
||||||
|
page_size = max(1, min(page_size, max_size))
|
||||||
|
current_page = max(1, current_page)
|
||||||
|
offset = (current_page - 1) * page_size
|
||||||
|
return current_page, page_size, offset
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_rows(result):
|
||||||
|
"""统一 sqlExe 查询结果为 list[dict]。"""
|
||||||
|
if result is None:
|
||||||
|
return []
|
||||||
|
if isinstance(result, list):
|
||||||
|
return result
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return [result]
|
||||||
|
return list(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _row_get(row, *keys, default=None):
|
||||||
|
"""兼容不同驱动返回的大小写字段名。"""
|
||||||
|
if not row:
|
||||||
|
return default
|
||||||
|
for key in keys:
|
||||||
|
if key in row:
|
||||||
|
return row[key]
|
||||||
|
lower = key.lower()
|
||||||
|
for k, v in row.items():
|
||||||
|
if k.lower() == lower:
|
||||||
|
return v
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_viewer(sor, accounting_orgid, userid=None):
|
||||||
|
if not userid:
|
||||||
|
return True, None
|
||||||
|
users = await sor.R('users', {'id': userid, 'del_flg': '0'})
|
||||||
|
if not users:
|
||||||
|
return False, '用户不存在'
|
||||||
|
user_orgid = users[0].get('orgid')
|
||||||
|
if user_orgid == accounting_orgid:
|
||||||
|
return True, None
|
||||||
|
parent = await get_parent_orgid(sor, accounting_orgid)
|
||||||
|
if parent and user_orgid == parent:
|
||||||
|
return True, None
|
||||||
|
return False, '无权查看该机构财务数据'
|
||||||
|
|
||||||
|
|
||||||
|
async def _org_name(sor, orgid):
|
||||||
|
if not orgid:
|
||||||
|
return None
|
||||||
|
rows = await sor.R('organization', {'id': orgid, 'del_flg': '0'})
|
||||||
|
return rows[0]['orgname'] if rows else None
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_business_owner(sor, orgid):
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
"SELECT id FROM organization WHERE id=${id}$ AND parentid IS NULL AND del_flg='0'",
|
||||||
|
{'id': orgid},
|
||||||
|
)
|
||||||
|
return len(rows) > 0
|
||||||
|
|
||||||
|
|
||||||
|
async def _collect_descendant_reseller_ids(sor, root_orgid):
|
||||||
|
"""递归收集 root 下所有下级分销商 organization.id(org_type=1)。"""
|
||||||
|
found = []
|
||||||
|
queue = [root_orgid]
|
||||||
|
seen = {root_orgid}
|
||||||
|
while queue:
|
||||||
|
pid = queue.pop(0)
|
||||||
|
rows = await sor.sqlExe(
|
||||||
|
"""SELECT id FROM organization
|
||||||
|
WHERE parentid=${pid}$ AND org_type=${org_type}$ AND del_flg='0'""",
|
||||||
|
{'pid': pid, 'org_type': RESELLER_ORG},
|
||||||
|
)
|
||||||
|
for r in rows:
|
||||||
|
cid = r['id']
|
||||||
|
if cid not in seen:
|
||||||
|
seen.add(cid)
|
||||||
|
found.append(cid)
|
||||||
|
queue.append(cid)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_customer_scope_sql(sor, accounting_orgid, include_sub_reseller_customers):
|
||||||
|
"""
|
||||||
|
客户范围 SQL 片段与参数。
|
||||||
|
直属:cust.parentid = accounting_orgid
|
||||||
|
含下级分销:直属 + cust.parentid IN (下级分销商 id 列表)
|
||||||
|
"""
|
||||||
|
params = {'accounting_orgid': accounting_orgid}
|
||||||
|
if not include_sub_reseller_customers:
|
||||||
|
return "cust.parentid = ${accounting_orgid}$", params, []
|
||||||
|
|
||||||
|
reseller_ids = await _collect_descendant_reseller_ids(sor, accounting_orgid)
|
||||||
|
if not reseller_ids:
|
||||||
|
return "cust.parentid = ${accounting_orgid}$", params, []
|
||||||
|
|
||||||
|
in_keys = []
|
||||||
|
for i, rid in enumerate(reseller_ids):
|
||||||
|
key = 'reseller_%d' % i
|
||||||
|
params[key] = rid
|
||||||
|
in_keys.append('${%s}$' % key)
|
||||||
|
in_sql = ', '.join(in_keys)
|
||||||
|
cond = "(cust.parentid = ${accounting_orgid}$ OR cust.parentid IN (%s))" % in_sql
|
||||||
|
return cond, params, reseller_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def _customer_in_scope(customer_parentid, accounting_orgid, reseller_ids, include_sub):
|
||||||
|
if customer_parentid == accounting_orgid:
|
||||||
|
return True
|
||||||
|
if include_sub and customer_parentid in reseller_ids:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_salemode_row(sor, sql, params):
|
||||||
|
rows = await sor.sqlExe(sql, params)
|
||||||
|
return rows[0] if rows else None
|
||||||
|
|
||||||
|
|
||||||
|
async def _settle_upstream_meta(sor, accounting_orgid, provider_orgid):
|
||||||
|
"""本级应付对象:本机构→供应商;分销→直接上级。"""
|
||||||
|
if await _is_business_owner(sor, accounting_orgid):
|
||||||
|
return {
|
||||||
|
'settle_upstream_type': 'supplier',
|
||||||
|
'settle_upstream_orgid': provider_orgid,
|
||||||
|
'settle_upstream_orgname': await _org_name(sor, provider_orgid),
|
||||||
|
'immediate_parent_orgid': None,
|
||||||
|
}
|
||||||
|
parent_orgid = await get_parent_orgid(sor, accounting_orgid)
|
||||||
|
return {
|
||||||
|
'settle_upstream_type': 'parent_org',
|
||||||
|
'settle_upstream_orgid': parent_orgid,
|
||||||
|
'settle_upstream_orgname': await _org_name(sor, parent_orgid),
|
||||||
|
'immediate_parent_orgid': parent_orgid,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _protocol_snapshot(sor, accounting_orgid, customerid, providerid, productid, bill_date, quantity):
|
||||||
|
curdate = bill_date
|
||||||
|
if hasattr(curdate, 'strftime'):
|
||||||
|
curdate = curdate.strftime('%Y-%m-%d')
|
||||||
|
elif curdate is not None:
|
||||||
|
curdate = str(curdate)[:10]
|
||||||
|
|
||||||
|
qty = int(quantity or 1)
|
||||||
|
base = {'providerid': providerid, 'productid': productid, 'curdate': curdate}
|
||||||
|
|
||||||
|
own = await _fetch_salemode_row(
|
||||||
|
sor, _SALEMODE_SQL_OWN, dict(base, bid_orgid=accounting_orgid),
|
||||||
|
)
|
||||||
|
cust = await _fetch_salemode_row(
|
||||||
|
sor, _SALEMODE_SQL_OFFER,
|
||||||
|
dict(base, offer_orgid=accounting_orgid, bid_orgid=customerid),
|
||||||
|
)
|
||||||
|
if not cust:
|
||||||
|
cust = await _fetch_salemode_row(
|
||||||
|
sor, _SALEMODE_SQL_OFFER,
|
||||||
|
dict(base, offer_orgid=accounting_orgid, bid_orgid='*'),
|
||||||
|
)
|
||||||
|
|
||||||
|
parent_orgid = await get_parent_orgid(sor, accounting_orgid)
|
||||||
|
reseller = None
|
||||||
|
if parent_orgid:
|
||||||
|
reseller = await _fetch_salemode_row(
|
||||||
|
sor, _SALEMODE_SQL_OFFER,
|
||||||
|
dict(base, offer_orgid=parent_orgid, bid_orgid=accounting_orgid),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'parent_orgid': parent_orgid,
|
||||||
|
'parent_orgname': await _org_name(sor, parent_orgid),
|
||||||
|
'own_salemode': own.get('salemode') if own else None,
|
||||||
|
'own_discount': own.get('discount') if own else None,
|
||||||
|
'own_floor_unit_price': own.get('price') if own else None,
|
||||||
|
'customer_salemode': cust.get('salemode') if cust else None,
|
||||||
|
'customer_discount': cust.get('discount') if cust else None,
|
||||||
|
'customer_floor_unit_price': cust.get('price') if cust else None,
|
||||||
|
'reseller_discount': reseller.get('discount') if reseller else None,
|
||||||
|
'reseller_salemode': reseller.get('salemode') if reseller else None,
|
||||||
|
'reseller_floor_unit_price': reseller.get('price') if reseller else None,
|
||||||
|
'quantity': qty,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _estimate_finance(catalog_amount, protocol, is_owner):
|
||||||
|
"""未记账:估算本级利润与本级应付(不含代付费单独字段)。"""
|
||||||
|
catalog_amount = float(catalog_amount or 0)
|
||||||
|
qty = protocol.get('quantity') or 1
|
||||||
|
profit = None
|
||||||
|
settle_upstream = None
|
||||||
|
|
||||||
|
own_mode = protocol.get('own_salemode')
|
||||||
|
cust_mode = protocol.get('customer_salemode')
|
||||||
|
own_disc = protocol.get('own_discount')
|
||||||
|
cust_disc = protocol.get('customer_discount')
|
||||||
|
reseller_disc = protocol.get('reseller_discount')
|
||||||
|
|
||||||
|
if own_mode == '0' or cust_mode == '0':
|
||||||
|
if own_disc is not None and cust_disc is not None:
|
||||||
|
profit = catalog_amount * (float(cust_disc) - float(own_disc))
|
||||||
|
if is_owner:
|
||||||
|
if own_disc is not None:
|
||||||
|
settle_upstream = catalog_amount * float(own_disc)
|
||||||
|
elif reseller_disc is not None:
|
||||||
|
settle_upstream = catalog_amount * float(reseller_disc)
|
||||||
|
|
||||||
|
elif own_mode == '2' or cust_mode == '2':
|
||||||
|
own_price = protocol.get('own_floor_unit_price')
|
||||||
|
cust_price = protocol.get('customer_floor_unit_price')
|
||||||
|
reseller_price = protocol.get('reseller_floor_unit_price')
|
||||||
|
if own_price is not None and cust_price is not None:
|
||||||
|
profit = (float(cust_price) - float(own_price)) * qty
|
||||||
|
if is_owner and own_price is not None:
|
||||||
|
settle_upstream = float(own_price) * qty
|
||||||
|
elif not is_owner and reseller_price is not None:
|
||||||
|
settle_upstream = float(reseller_price) * qty
|
||||||
|
elif not is_owner and cust_price is not None:
|
||||||
|
settle_upstream = float(cust_price) * qty
|
||||||
|
|
||||||
|
return {
|
||||||
|
'profit_amount': _round_money(profit),
|
||||||
|
'settle_upstream_amount': _round_money(settle_upstream),
|
||||||
|
'amount_source': 'estimated',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _finance_from_bill_detail(sor, billid, accounting_orgid, is_owner, parent_orgid):
|
||||||
|
"""已记账:从 bill_detail 取本级利润与本级应付。"""
|
||||||
|
sql = """
|
||||||
|
SELECT accounting_orgid, subjectname, accounting_dir, participantid, participanttype, amount
|
||||||
|
FROM bill_detail WHERE billid = ${billid}$ AND del_flg = '0'
|
||||||
|
"""
|
||||||
|
rows = await sor.sqlExe(sql, {'billid': billid})
|
||||||
|
profit = 0.0
|
||||||
|
settle_upstream = 0.0
|
||||||
|
legs = []
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
legs.append({
|
||||||
|
'accounting_orgid': r['accounting_orgid'],
|
||||||
|
'subjectname': r['subjectname'],
|
||||||
|
'accounting_dir': r['accounting_dir'],
|
||||||
|
'participanttype': r.get('participanttype'),
|
||||||
|
'participantid': r.get('participantid'),
|
||||||
|
'amount': _round_money(r['amount']),
|
||||||
|
})
|
||||||
|
amt = float(r['amount'] or 0)
|
||||||
|
book = r['accounting_orgid']
|
||||||
|
subj = r['subjectname'] or ''
|
||||||
|
direction = r['accounting_dir']
|
||||||
|
|
||||||
|
if book == accounting_orgid and subj in INCOME_SUBJECTS and direction == '贷':
|
||||||
|
profit += amt
|
||||||
|
|
||||||
|
if is_owner:
|
||||||
|
if (
|
||||||
|
book == accounting_orgid
|
||||||
|
and subj.startswith(SUPPLIER_SETTLE_PREFIX)
|
||||||
|
and direction == '贷'
|
||||||
|
):
|
||||||
|
settle_upstream += amt
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
parent_orgid
|
||||||
|
and book == parent_orgid
|
||||||
|
and subj == PARENT_SETTLE_SUBJECT
|
||||||
|
and direction == '借'
|
||||||
|
and r.get('participantid') == accounting_orgid
|
||||||
|
):
|
||||||
|
settle_upstream += amt
|
||||||
|
|
||||||
|
return {
|
||||||
|
'profit_amount': _round_money(profit),
|
||||||
|
'settle_upstream_amount': _round_money(settle_upstream) if settle_upstream else _round_money(0),
|
||||||
|
'amount_source': 'bill_detail',
|
||||||
|
'bill_detail_legs': legs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_report_row(sor, row, accounting_orgid, is_owner):
|
||||||
|
bill_id = _row_get(row, 'bill_id')
|
||||||
|
customerid = _row_get(row, 'customerid')
|
||||||
|
providerid = _row_get(row, 'providerid')
|
||||||
|
productid = _row_get(row, 'productid')
|
||||||
|
bill_date = _row_get(row, 'bill_date')
|
||||||
|
quantity = _row_get(row, 'quantity') or 1
|
||||||
|
catalog_amount = _round_money(_row_get(row, 'catalog_amount'))
|
||||||
|
customer_pay = _round_money(_row_get(row, 'customer_pay_amount'))
|
||||||
|
customer_parentid = _row_get(row, 'customer_parentid')
|
||||||
|
|
||||||
|
protocol = await _protocol_snapshot(
|
||||||
|
sor, accounting_orgid, customerid, providerid, productid, bill_date, quantity,
|
||||||
|
)
|
||||||
|
parent_orgid = protocol['parent_orgid']
|
||||||
|
settle_meta = await _settle_upstream_meta(sor, accounting_orgid, providerid)
|
||||||
|
|
||||||
|
bill_state = _row_get(row, 'bill_state')
|
||||||
|
if str(bill_state) == '1':
|
||||||
|
amounts = await _finance_from_bill_detail(
|
||||||
|
sor, bill_id, accounting_orgid, is_owner, parent_orgid,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
amounts = _estimate_finance(catalog_amount, protocol, is_owner)
|
||||||
|
amounts['bill_detail_legs'] = []
|
||||||
|
|
||||||
|
product_name = _row_get(row, 'product_name')
|
||||||
|
if not product_name and productid:
|
||||||
|
prows = await sor.R('product', {'id': productid, 'del_flg': '0'})
|
||||||
|
if prows:
|
||||||
|
product_name = prows[0].get('name')
|
||||||
|
provider_name = _row_get(row, 'provider_name')
|
||||||
|
if not provider_name and providerid:
|
||||||
|
prov_rows = await sor.sqlExe(
|
||||||
|
"SELECT name FROM provider WHERE orgid=${orgid}$ AND del_flg='0' LIMIT 1",
|
||||||
|
{'orgid': providerid},
|
||||||
|
)
|
||||||
|
if prov_rows:
|
||||||
|
provider_name = prov_rows[0].get('name')
|
||||||
|
|
||||||
|
serving_reseller_id = None
|
||||||
|
serving_reseller_name = None
|
||||||
|
is_direct = customer_parentid == accounting_orgid
|
||||||
|
if not is_direct and customer_parentid:
|
||||||
|
serving_reseller_id = customer_parentid
|
||||||
|
serving_reseller_name = await _org_name(sor, customer_parentid)
|
||||||
|
|
||||||
|
order_date = _row_get(row, 'order_date')
|
||||||
|
servicename = _row_get(row, 'servicename')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bill_id': bill_id,
|
||||||
|
'order_id': _row_get(row, 'order_id'),
|
||||||
|
'bill_date': str(bill_date)[:10] if bill_date else None,
|
||||||
|
'order_date': str(order_date)[:19] if order_date else None,
|
||||||
|
'business_op': _row_get(row, 'business_op'),
|
||||||
|
'bill_state': bill_state,
|
||||||
|
'accounted': str(bill_state) == '1',
|
||||||
|
'customer': {
|
||||||
|
'id': customerid,
|
||||||
|
'name': _row_get(row, 'customer_name'),
|
||||||
|
'parent_orgid': customer_parentid,
|
||||||
|
'is_direct_customer': is_direct,
|
||||||
|
'serving_reseller': {
|
||||||
|
'id': serving_reseller_id,
|
||||||
|
'name': serving_reseller_name,
|
||||||
|
} if serving_reseller_id else None,
|
||||||
|
},
|
||||||
|
'product': {
|
||||||
|
'id': productid,
|
||||||
|
'name': product_name or servicename,
|
||||||
|
'servicename': servicename,
|
||||||
|
},
|
||||||
|
'provider': {
|
||||||
|
'orgid': providerid,
|
||||||
|
'name': provider_name,
|
||||||
|
},
|
||||||
|
'quantity': quantity,
|
||||||
|
'pricing': {
|
||||||
|
'catalog_amount': catalog_amount,
|
||||||
|
'list_price_unit': _round_money(_row_get(row, 'list_price')),
|
||||||
|
'order_discount': _round_money(_row_get(row, 'order_discount'))
|
||||||
|
if _row_get(row, 'order_discount') is not None
|
||||||
|
else None,
|
||||||
|
'order_unit_price': _round_money(_row_get(row, 'order_unit_price')),
|
||||||
|
'customer_pay_amount': customer_pay,
|
||||||
|
},
|
||||||
|
'viewer_org': {
|
||||||
|
'id': accounting_orgid,
|
||||||
|
'is_business_owner': is_owner,
|
||||||
|
'immediate_parent': {
|
||||||
|
'id': settle_meta.get('immediate_parent_orgid'),
|
||||||
|
'name': await _org_name(sor, settle_meta.get('immediate_parent_orgid')),
|
||||||
|
} if settle_meta.get('immediate_parent_orgid') else None,
|
||||||
|
},
|
||||||
|
'protocol': {
|
||||||
|
'parent_salemode': _salemode_label(protocol.get('own_salemode')),
|
||||||
|
'parent_discount_to_us': _round_money(protocol.get('own_discount')),
|
||||||
|
'parent_floor_unit_price': _round_money(protocol.get('own_floor_unit_price')),
|
||||||
|
'our_salemode_to_customer': _salemode_label(protocol.get('customer_salemode')),
|
||||||
|
'our_discount_to_customer': _round_money(protocol.get('customer_discount')),
|
||||||
|
'our_floor_unit_price_to_customer': _round_money(protocol.get('customer_floor_unit_price')),
|
||||||
|
'our_discount_as_reseller_to_parent': _round_money(protocol.get('reseller_discount')),
|
||||||
|
},
|
||||||
|
'finance': {
|
||||||
|
'profit_amount': amounts['profit_amount'],
|
||||||
|
'settle_upstream_amount': amounts['settle_upstream_amount'],
|
||||||
|
'settle_upstream_type': settle_meta['settle_upstream_type'],
|
||||||
|
'settle_upstream_target': {
|
||||||
|
'id': settle_meta['settle_upstream_orgid'],
|
||||||
|
'name': settle_meta['settle_upstream_orgname'],
|
||||||
|
},
|
||||||
|
'amount_source': amounts['amount_source'],
|
||||||
|
},
|
||||||
|
'bill_detail_legs': amounts.get('bill_detail_legs', []),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bill_core_from_clause():
|
||||||
|
"""统计/查 ID 用最小 JOIN(兼容无 ordergoodsid 字段的库)。"""
|
||||||
|
return """
|
||||||
|
FROM bill b
|
||||||
|
INNER JOIN bz_order o ON b.orderid = o.id
|
||||||
|
INNER JOIN organization cust ON b.customerid = cust.id
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _bill_ids_sql(where_sql):
|
||||||
|
"""不在 SQL 里写 LIMIT:部分环境 sqlExe 对 LIMIT 会返回空列表。"""
|
||||||
|
return f"""
|
||||||
|
SELECT b.id AS bill_id
|
||||||
|
{_bill_core_from_clause()}
|
||||||
|
WHERE {where_sql}
|
||||||
|
ORDER BY b.bill_date DESC, b.create_at DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _count_sql(where_sql):
|
||||||
|
return f"""
|
||||||
|
SELECT COUNT(*) AS total_count
|
||||||
|
{_bill_core_from_clause()}
|
||||||
|
WHERE {where_sql}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_bill_id(bill_id):
|
||||||
|
if bill_id is None:
|
||||||
|
return None
|
||||||
|
s = str(bill_id).strip()
|
||||||
|
return s if s else None
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_bill_row_via_R(sor, bill_id):
|
||||||
|
"""
|
||||||
|
用 sor.R 按表组装明细(与 mu_ban / process_user_billing 一致)。
|
||||||
|
已证实:本环境 sqlExe 多表 JOIN + WHERE b.id 会返回空,但 sor.R('bill') 可用。
|
||||||
|
"""
|
||||||
|
bid = _normalize_bill_id(bill_id)
|
||||||
|
if not bid:
|
||||||
|
return None
|
||||||
|
|
||||||
|
bills = await sor.R('bill', {'id': bid, 'del_flg': '0'})
|
||||||
|
if not bills:
|
||||||
|
bills = await sor.R('bill', {'id': bid})
|
||||||
|
if not bills:
|
||||||
|
return None
|
||||||
|
b = bills[0]
|
||||||
|
|
||||||
|
o = {}
|
||||||
|
oid = b.get('orderid')
|
||||||
|
if oid:
|
||||||
|
orders = await sor.R('bz_order', {'id': oid, 'del_flg': '0'})
|
||||||
|
if not orders:
|
||||||
|
orders = await sor.R('bz_order', {'id': oid})
|
||||||
|
if orders:
|
||||||
|
o = orders[0]
|
||||||
|
|
||||||
|
cust = {}
|
||||||
|
cid = b.get('customerid')
|
||||||
|
if cid:
|
||||||
|
custs = await sor.R('organization', {'id': cid, 'del_flg': '0'})
|
||||||
|
if not custs:
|
||||||
|
custs = await sor.R('organization', {'id': cid})
|
||||||
|
if custs:
|
||||||
|
cust = custs[0]
|
||||||
|
|
||||||
|
og = {}
|
||||||
|
ogid = b.get('ordergoodsid')
|
||||||
|
if ogid:
|
||||||
|
ogs = await sor.R('order_goods', {'id': ogid, 'del_flg': '0'})
|
||||||
|
if ogs:
|
||||||
|
og = ogs[0]
|
||||||
|
if not og and oid:
|
||||||
|
ogs = await sor.sqlExe(
|
||||||
|
"""SELECT list_price, discount, price FROM order_goods
|
||||||
|
WHERE orderid=${oid}$ AND del_flg='0' LIMIT 1""",
|
||||||
|
{'oid': oid},
|
||||||
|
)
|
||||||
|
if ogs:
|
||||||
|
og = ogs[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'bill_id': b.get('id'),
|
||||||
|
'order_id': oid,
|
||||||
|
'customerid': cid,
|
||||||
|
'productid': b.get('productid'),
|
||||||
|
'providerid': b.get('providerid'),
|
||||||
|
'bill_date': b.get('bill_date'),
|
||||||
|
'bill_state': b.get('bill_state'),
|
||||||
|
'catalog_amount': b.get('provider_amt'),
|
||||||
|
'customer_pay_amount': b.get('amount'),
|
||||||
|
'quantity': b.get('quantity'),
|
||||||
|
'order_date': o.get('order_date'),
|
||||||
|
'business_op': o.get('business_op'),
|
||||||
|
'servicename': o.get('servicename'),
|
||||||
|
'customer_name': cust.get('orgname'),
|
||||||
|
'customer_parentid': cust.get('parentid'),
|
||||||
|
'list_price': og.get('list_price'),
|
||||||
|
'order_discount': og.get('discount'),
|
||||||
|
'order_unit_price': og.get('price'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_bill_rows_by_ids(sor, bill_ids):
|
||||||
|
rows = []
|
||||||
|
for bid in bill_ids:
|
||||||
|
one = await _fetch_bill_row_via_R(sor, bid)
|
||||||
|
if one:
|
||||||
|
rows.append(one)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_bill_rows_page(sor, where_sql, base_params, offset, page_size):
|
||||||
|
"""
|
||||||
|
先查全部符合条件的 bill_id(通常数量可控),再在 Python 分页;
|
||||||
|
明细用 IN 字面量或逐条查询,不用 IN (${bid_n}$) 占位符。
|
||||||
|
"""
|
||||||
|
id_sql = _bill_ids_sql(where_sql)
|
||||||
|
id_rows = _sql_rows(await sor.sqlExe(id_sql, base_params))
|
||||||
|
all_ids = []
|
||||||
|
seen = set()
|
||||||
|
for r in id_rows:
|
||||||
|
bid = _normalize_bill_id(_row_get(r, 'bill_id', 'id', 'ID'))
|
||||||
|
if bid and bid not in seen:
|
||||||
|
seen.add(bid)
|
||||||
|
all_ids.append(bid)
|
||||||
|
total = len(all_ids)
|
||||||
|
page_ids = all_ids[offset: offset + page_size]
|
||||||
|
if not page_ids:
|
||||||
|
return total, []
|
||||||
|
ordered = await _fetch_bill_rows_by_ids(sor, page_ids)
|
||||||
|
return total, ordered
|
||||||
|
|
||||||
|
|
||||||
|
async def _finance_report_debug(sor, where_sql, params, page_size, offset):
|
||||||
|
"""debug=true 时返回,便于在服务器上对比哪一步 SQL 无数据。"""
|
||||||
|
dbg = {}
|
||||||
|
try:
|
||||||
|
c = _sql_rows(await sor.sqlExe(_count_sql(where_sql), params))
|
||||||
|
dbg['count_total'] = int(_row_get(c[0], 'total_count', 0) or 0) if c else None
|
||||||
|
except Exception as e:
|
||||||
|
dbg['count_error'] = str(e)
|
||||||
|
try:
|
||||||
|
ids = _sql_rows(await sor.sqlExe(_bill_ids_sql(where_sql), params))
|
||||||
|
dbg['ids_query_len'] = len(ids)
|
||||||
|
except Exception as e:
|
||||||
|
dbg['ids_error'] = str(e)
|
||||||
|
try:
|
||||||
|
lim_sql = _bill_ids_sql(where_sql).strip() + ' LIMIT %d OFFSET %d' % (page_size, offset)
|
||||||
|
lim = _sql_rows(await sor.sqlExe(lim_sql, params))
|
||||||
|
dbg['ids_with_limit_len'] = len(lim)
|
||||||
|
except Exception as e:
|
||||||
|
dbg['ids_with_limit_error'] = str(e)
|
||||||
|
try:
|
||||||
|
page_ids = []
|
||||||
|
id_rows = _sql_rows(await sor.sqlExe(_bill_ids_sql(where_sql), params))
|
||||||
|
if id_rows:
|
||||||
|
dbg['first_id_row_keys'] = list(id_rows[0].keys())
|
||||||
|
dbg['first_id_raw'] = _row_get(id_rows[0], 'bill_id', 'id', 'ID')
|
||||||
|
for r in id_rows[offset: offset + page_size]:
|
||||||
|
bid = _normalize_bill_id(_row_get(r, 'bill_id', 'id', 'ID'))
|
||||||
|
if bid:
|
||||||
|
page_ids.append(bid)
|
||||||
|
dbg['page_ids_len'] = len(page_ids)
|
||||||
|
if page_ids:
|
||||||
|
dbg['sample_page_id'] = page_ids[0]
|
||||||
|
rb = await sor.R('bill', {'id': page_ids[0], 'del_flg': '0'})
|
||||||
|
dbg['sor_R_bill_count'] = len(rb) if rb else 0
|
||||||
|
one = await _fetch_bill_row_via_R(sor, page_ids[0])
|
||||||
|
dbg['sor_R_row_ok'] = one is not None
|
||||||
|
except Exception as e:
|
||||||
|
dbg['detail_fetch_error'] = str(e)
|
||||||
|
return dbg
|
||||||
|
|
||||||
|
|
||||||
|
async def finance_order_report(ns=None):
|
||||||
|
"""
|
||||||
|
管理人员 — 客户订单财务列表(分页)。
|
||||||
|
|
||||||
|
入参 ns:
|
||||||
|
accounting_orgid (必填) 账本机构
|
||||||
|
include_sub_reseller_customers (可选) 默认 false;true 时含下级分销商的客户订单
|
||||||
|
userid, start_date, end_date, customerid, productid, order_id, bill_state
|
||||||
|
current_page (默认1), page_size (默认20, 最大100)
|
||||||
|
|
||||||
|
返回 finance.settle_upstream_*:
|
||||||
|
- 本机构:应付供应商(待结转* 贷方,仅本机构账本)
|
||||||
|
- 分销商:应付直接上级(上级账本「分销商存放资金」借方,participant=本级)
|
||||||
|
"""
|
||||||
|
ns = ns or {}
|
||||||
|
accounting_orgid = ns.get('accounting_orgid')
|
||||||
|
if not accounting_orgid:
|
||||||
|
return {'status': False, 'msg': '缺少 accounting_orgid(当前账本机构)'}
|
||||||
|
|
||||||
|
include_sub = _parse_bool(ns.get('include_sub_reseller_customers'), False)
|
||||||
|
current_page, page_size, offset = _parse_page(ns)
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
ok, err = await _check_viewer(sor, accounting_orgid, ns.get('userid'))
|
||||||
|
if not ok:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
scope_sql, scope_params, reseller_ids = await _build_customer_scope_sql(
|
||||||
|
sor, accounting_orgid, include_sub,
|
||||||
|
)
|
||||||
|
is_owner = await _is_business_owner(sor, accounting_orgid)
|
||||||
|
|
||||||
|
conditions = ["b.del_flg = '0'", "o.del_flg = '0'", scope_sql]
|
||||||
|
params = dict(scope_params)
|
||||||
|
|
||||||
|
if ns.get('start_date'):
|
||||||
|
conditions.append('b.bill_date >= ${start_date}$')
|
||||||
|
params['start_date'] = ns['start_date']
|
||||||
|
if ns.get('end_date'):
|
||||||
|
conditions.append('b.bill_date <= ${end_date}$')
|
||||||
|
params['end_date'] = ns['end_date']
|
||||||
|
if ns.get('customerid'):
|
||||||
|
conditions.append('b.customerid = ${customerid}$')
|
||||||
|
params['customerid'] = ns['customerid']
|
||||||
|
if ns.get('productid'):
|
||||||
|
conditions.append('b.productid = ${productid}$')
|
||||||
|
params['productid'] = ns['productid']
|
||||||
|
if ns.get('order_id'):
|
||||||
|
conditions.append('b.orderid = ${order_id}$')
|
||||||
|
params['order_id'] = ns['order_id']
|
||||||
|
if ns.get('bill_state') is not None:
|
||||||
|
conditions.append('b.bill_state = ${bill_state}$')
|
||||||
|
params['bill_state'] = str(ns['bill_state'])
|
||||||
|
|
||||||
|
where_sql = ' AND '.join(conditions)
|
||||||
|
|
||||||
|
total_count, rows = await _fetch_bill_rows_page(
|
||||||
|
sor, where_sql, params, offset, page_size,
|
||||||
|
)
|
||||||
|
if total_count > 0 and offset >= total_count:
|
||||||
|
current_page = 1
|
||||||
|
offset = 0
|
||||||
|
total_count, rows = await _fetch_bill_rows_page(
|
||||||
|
sor, where_sql, params, offset, page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
debug_info = None
|
||||||
|
if _parse_bool(ns.get('debug'), False):
|
||||||
|
debug_info = await _finance_report_debug(
|
||||||
|
sor, where_sql, params, page_size, offset,
|
||||||
|
)
|
||||||
|
debug_info['page_ids_fetched'] = len(rows)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
sum_profit = 0.0
|
||||||
|
sum_pay = 0.0
|
||||||
|
sum_upstream = 0.0
|
||||||
|
row_errors = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
item = await _build_report_row(sor, row, accounting_orgid, is_owner)
|
||||||
|
except Exception as exc:
|
||||||
|
row_errors.append({
|
||||||
|
'bill_id': _row_get(row, 'bill_id'),
|
||||||
|
'error': str(exc),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
items.append(item)
|
||||||
|
fin = item['finance']
|
||||||
|
if fin.get('profit_amount') is not None:
|
||||||
|
sum_profit += fin['profit_amount']
|
||||||
|
pay = item['pricing'].get('customer_pay_amount')
|
||||||
|
if pay is not None:
|
||||||
|
sum_pay += pay
|
||||||
|
up = fin.get('settle_upstream_amount')
|
||||||
|
if up is not None:
|
||||||
|
sum_upstream += up
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'accounting_orgid': accounting_orgid,
|
||||||
|
'accounting_orgname': await _org_name(sor, accounting_orgid),
|
||||||
|
'is_business_owner': is_owner,
|
||||||
|
'customer_scope': {
|
||||||
|
'include_sub_reseller_customers': include_sub,
|
||||||
|
'descendant_reseller_ids': reseller_ids,
|
||||||
|
'descendant_reseller_count': len(reseller_ids),
|
||||||
|
},
|
||||||
|
'total_count': total_count,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'summary': {
|
||||||
|
'customer_pay_total': _round_money(sum_pay),
|
||||||
|
'profit_total': _round_money(sum_profit),
|
||||||
|
'settle_upstream_total': _round_money(sum_upstream),
|
||||||
|
},
|
||||||
|
'items': items,
|
||||||
|
'row_errors': row_errors,
|
||||||
|
'list_row_count': len(rows),
|
||||||
|
'debug': debug_info,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def finance_order_report_detail(ns=None):
|
||||||
|
"""
|
||||||
|
管理人员 — 单笔账单财务明细。
|
||||||
|
|
||||||
|
入参:accounting_orgid, bill_id, include_sub_reseller_customers(与列表一致,用于校验客户范围)
|
||||||
|
"""
|
||||||
|
ns = ns or {}
|
||||||
|
accounting_orgid = ns.get('accounting_orgid')
|
||||||
|
bill_id = ns.get('bill_id')
|
||||||
|
if not accounting_orgid or not bill_id:
|
||||||
|
return {'status': False, 'msg': '缺少 accounting_orgid 或 bill_id'}
|
||||||
|
|
||||||
|
include_sub = _parse_bool(ns.get('include_sub_reseller_customers'), False)
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(DBNAME) as sor:
|
||||||
|
ok, err = await _check_viewer(sor, accounting_orgid, ns.get('userid'))
|
||||||
|
if not ok:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
_, _, reseller_ids = await _build_customer_scope_sql(sor, accounting_orgid, include_sub)
|
||||||
|
is_owner = await _is_business_owner(sor, accounting_orgid)
|
||||||
|
|
||||||
|
rows = await _fetch_bill_rows_by_ids(sor, [bill_id])
|
||||||
|
if not rows:
|
||||||
|
return {'status': False, 'msg': '账单不存在'}
|
||||||
|
|
||||||
|
row = rows[0]
|
||||||
|
if not await _customer_in_scope(
|
||||||
|
_row_get(row, 'customer_parentid'),
|
||||||
|
accounting_orgid,
|
||||||
|
reseller_ids,
|
||||||
|
include_sub,
|
||||||
|
):
|
||||||
|
return {'status': False, 'msg': '该账单不在当前查询客户范围内'}
|
||||||
|
|
||||||
|
detail = await _build_report_row(sor, row, accounting_orgid, is_owner)
|
||||||
|
detail['customer_scope'] = {'include_sub_reseller_customers': include_sub}
|
||||||
|
return {'status': True, 'msg': 'ok', 'data': detail}
|
||||||
|
|
||||||
|
|
||||||
|
ret = await finance_order_report(params_kw)
|
||||||
|
return ret
|
||||||
1231
b/bill/finance_order_report_detail.dspy
Normal file
1231
b/bill/finance_order_report_detail.dspy
Normal file
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@ async def cumulative_order(ns={}):
|
|||||||
|
|
||||||
# 统计全部 累计支付金额和累计优惠金额 不包含各种筛选条件
|
# 统计全部 累计支付金额和累计优惠金额 不包含各种筛选条件
|
||||||
# 累计支付金额=BUY+RENEW-BUY_REVERSE并且对应order_status=1是实际支付金额 累计优惠金额=BUY+RENEW-BUY_REVERSE并且对应order_status=1是优惠金额
|
# 累计支付金额=BUY+RENEW-BUY_REVERSE并且对应order_status=1是实际支付金额 累计优惠金额=BUY+RENEW-BUY_REVERSE并且对应order_status=1是优惠金额
|
||||||
|
# 不包含大模型订单
|
||||||
total_amount_sql = """
|
total_amount_sql = """
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(
|
COALESCE(SUM(
|
||||||
@ -33,6 +34,7 @@ async def cumulative_order(ns={}):
|
|||||||
JOIN bz_order bo ON og.orderid = bo.id
|
JOIN bz_order bo ON og.orderid = bo.id
|
||||||
WHERE og.del_flg = '0'
|
WHERE og.del_flg = '0'
|
||||||
AND bo.del_flg = '0'
|
AND bo.del_flg = '0'
|
||||||
|
AND og.is_big_model = 0
|
||||||
AND bo.customerid = ${customerid}$
|
AND bo.customerid = ${customerid}$
|
||||||
"""
|
"""
|
||||||
total_amount_result = await sor.sqlExe(total_amount_sql, {'customerid': customerid})
|
total_amount_result = await sor.sqlExe(total_amount_sql, {'customerid': customerid})
|
||||||
|
|||||||
@ -74,8 +74,13 @@ async def _load_session_messages(sor, session_id):
|
|||||||
async def _resolve_chat_config(ns, sor):
|
async def _resolve_chat_config(ns, sor):
|
||||||
# api_url = ns.get('api_url')
|
# api_url = ns.get('api_url')
|
||||||
# api_key = ns.get('api_key')
|
# api_key = ns.get('api_key')
|
||||||
api_url = 'https://api.deepseek.com/chat/completions'
|
# api_url = 'https://api.deepseek.com/chat/completions'
|
||||||
api_key = 'sk-c22d6573e85a4d3fa8ab932386cf2909'
|
# api_key = 'sk-c22d6573e85a4d3fa8ab932386cf2909'
|
||||||
|
|
||||||
|
|
||||||
|
api_url = 'https://ai.atvoe.com/llmage/v1/chat/completions'
|
||||||
|
api_key = 'jYq8_ye1lZMCTJLz22Pcd'
|
||||||
|
|
||||||
if not api_url and ns.get('model_id'):
|
if not api_url and ns.get('model_id'):
|
||||||
doc_rows = await sor.sqlExe(
|
doc_rows = await sor.sqlExe(
|
||||||
"SELECT api_url FROM model_api_doc WHERE model_id = '%s' LIMIT 1;"
|
"SELECT api_url FROM model_api_doc WHERE model_id = '%s' LIMIT 1;"
|
||||||
@ -199,7 +204,9 @@ async def inference_generator(request, params_kw=None, **kw):
|
|||||||
|
|
||||||
ns = params_kw or {}
|
ns = params_kw or {}
|
||||||
# model = ns.get('model')
|
# model = ns.get('model')
|
||||||
model = 'deepseek-v4-pro'
|
# model = 'deepseek-v4-pro'
|
||||||
|
model = 'qwen3.6-plus'
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
yield _sse_event({'type': 'error', 'msg': 'model is required'})
|
yield _sse_event({'type': 'error', 'msg': 'model is required'})
|
||||||
yield 'data: [DONE]\n\n'
|
yield 'data: [DONE]\n\n'
|
||||||
|
|||||||
512
b/cntoai/model_usage_admin_report.dspy
Normal file
512
b/cntoai/model_usage_admin_report.dspy
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
def _escape(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value).replace("'", "''")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_usage_content(raw):
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
return raw
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_time_range(ns):
|
||||||
|
"""
|
||||||
|
解析时间范围。
|
||||||
|
|
||||||
|
优先使用 start_time + end_time;否则按 range 快捷窗口:
|
||||||
|
hour -> 最近 1 小时
|
||||||
|
day -> 最近 1 天
|
||||||
|
week -> 最近 7 天
|
||||||
|
默认 day。
|
||||||
|
"""
|
||||||
|
start_time = ns.get('start_time')
|
||||||
|
end_time = ns.get('end_time')
|
||||||
|
range_type = (ns.get('range') or ns.get('range_type') or 'day').lower()
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if start_time and end_time:
|
||||||
|
start_dt = _parse_datetime(start_time)
|
||||||
|
end_dt = _parse_datetime(end_time, end_of_day=True)
|
||||||
|
if not start_dt or not end_dt:
|
||||||
|
return None, None, 'start_time / end_time 格式无效,请使用 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS'
|
||||||
|
if start_dt > end_dt:
|
||||||
|
return None, None, 'start_time 不能晚于 end_time'
|
||||||
|
return start_dt, end_dt, None
|
||||||
|
|
||||||
|
delta_map = {
|
||||||
|
'hour': datetime.timedelta(hours=1),
|
||||||
|
'day': datetime.timedelta(days=1),
|
||||||
|
'week': datetime.timedelta(weeks=1),
|
||||||
|
}
|
||||||
|
if range_type not in delta_map:
|
||||||
|
return None, None, 'range 仅支持 hour / day / week'
|
||||||
|
|
||||||
|
end_dt = now
|
||||||
|
start_dt = now - delta_map[range_type]
|
||||||
|
return start_dt, end_dt, None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_datetime(value, end_of_day=False):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
text = str(value).strip()
|
||||||
|
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d'):
|
||||||
|
try:
|
||||||
|
dt = datetime.datetime.strptime(text, fmt)
|
||||||
|
if fmt == '%Y-%m-%d' and end_of_day:
|
||||||
|
dt = dt.replace(hour=23, minute=59, second=59)
|
||||||
|
return dt
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_datetime(dt):
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
|
def _group_key(dt, group_by):
|
||||||
|
if group_by == 'hour':
|
||||||
|
return dt.strftime('%Y-%m-%d %H:00:00')
|
||||||
|
if group_by == 'week':
|
||||||
|
monday = dt - datetime.timedelta(days=dt.weekday())
|
||||||
|
return monday.strftime('%Y-%m-%d')
|
||||||
|
return dt.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_usage_row(row, bill_amount_map=None):
|
||||||
|
usage = _parse_usage_content(row.get('usage_content'))
|
||||||
|
orderid = row.get('orderid')
|
||||||
|
amount = float(row.get('original_price') or 0)
|
||||||
|
if bill_amount_map and orderid and orderid in bill_amount_map:
|
||||||
|
amount = float(bill_amount_map[orderid] or amount)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': row.get('id'),
|
||||||
|
'userid': row.get('userid'),
|
||||||
|
'llmid': row.get('llmid'),
|
||||||
|
'model': usage.get('model') or row.get('model_name') or row.get('llm_model'),
|
||||||
|
'prompt_tokens': int(usage.get('prompt_tokens') or 0),
|
||||||
|
'completion_tokens': int(usage.get('completion_tokens') or 0),
|
||||||
|
'total_tokens': int(usage.get('total_tokens') or 0),
|
||||||
|
'amount': round(amount, 8),
|
||||||
|
'bill_status': row.get('bill_status'),
|
||||||
|
'orderid': orderid,
|
||||||
|
'usage_time': row.get('created_at'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_bill_amount_map(sor, order_ids):
|
||||||
|
if not order_ids:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for orderid in set(order_ids):
|
||||||
|
if not orderid:
|
||||||
|
continue
|
||||||
|
rows = await sor.R('bill', {'orderid': orderid, 'del_flg': '0'})
|
||||||
|
if rows:
|
||||||
|
result[orderid] = sum(float(r.get('amount') or 0) for r in rows)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_llm_model_map(sor, llm_ids):
|
||||||
|
result = {}
|
||||||
|
for llmid in set(llm_ids):
|
||||||
|
if not llmid:
|
||||||
|
continue
|
||||||
|
rows = await sor.R('llm', {'id': llmid})
|
||||||
|
if rows:
|
||||||
|
result[llmid] = rows[0].get('model')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _build_usage_where(userid=None, userids=None, start_dt=None, end_dt=None, model_filter=None):
|
||||||
|
"""构建 model_usage 单表查询条件(不使用 JOIN,兼容 sqlExe 环境)。"""
|
||||||
|
conditions = []
|
||||||
|
if userid:
|
||||||
|
conditions.append("userid = '%s'" % _escape(userid))
|
||||||
|
elif userids:
|
||||||
|
escaped = ["'%s'" % _escape(uid) for uid in userids if uid]
|
||||||
|
if not escaped:
|
||||||
|
conditions.append('1 = 0')
|
||||||
|
else:
|
||||||
|
conditions.append('userid IN (%s)' % ','.join(escaped))
|
||||||
|
if start_dt and end_dt:
|
||||||
|
conditions.append(
|
||||||
|
"created_at BETWEEN '%s' AND '%s'"
|
||||||
|
% (_format_datetime(start_dt), _format_datetime(end_dt))
|
||||||
|
)
|
||||||
|
if model_filter:
|
||||||
|
conditions.append(
|
||||||
|
"JSON_UNQUOTE(JSON_EXTRACT(usage_content, '$.model')) = '%s'"
|
||||||
|
% _escape(model_filter)
|
||||||
|
)
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_model_usage_rows(sor, conditions, limit=None, offset=None):
|
||||||
|
where_clause = ' AND '.join(conditions) if conditions else '1 = 1'
|
||||||
|
sql = """
|
||||||
|
SELECT id, userid, llmid, original_price, orderid, bill_status, usage_content, created_at
|
||||||
|
FROM model_usage
|
||||||
|
WHERE %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
""" % where_clause
|
||||||
|
if limit is not None:
|
||||||
|
sql += ' LIMIT %d OFFSET %d' % (int(limit), int(offset or 0))
|
||||||
|
return await sor.sqlExe(sql, {})
|
||||||
|
|
||||||
|
|
||||||
|
async def _count_model_usage(sor, conditions):
|
||||||
|
where_clause = ' AND '.join(conditions) if conditions else '1 = 1'
|
||||||
|
sql = 'SELECT COUNT(*) AS total_count FROM model_usage WHERE %s' % where_clause
|
||||||
|
return (await sor.sqlExe(sql, {}))[0]['total_count']
|
||||||
|
|
||||||
|
|
||||||
|
async def _enrich_usage_rows(sor, rows):
|
||||||
|
order_ids = [row.get('orderid') for row in rows if row.get('orderid')]
|
||||||
|
llm_ids = [row.get('llmid') for row in rows if row.get('llmid')]
|
||||||
|
bill_amount_map = await _fetch_bill_amount_map(sor, order_ids)
|
||||||
|
llm_model_map = await _fetch_llm_model_map(sor, llm_ids)
|
||||||
|
items = []
|
||||||
|
for row in rows:
|
||||||
|
enriched = dict(row)
|
||||||
|
enriched['llm_model'] = llm_model_map.get(row.get('llmid'))
|
||||||
|
items.append(_normalize_usage_row(enriched, bill_amount_map))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_customer_users(sor, orgid, customerid=None):
|
||||||
|
"""获取机构下客户及其用户映射。"""
|
||||||
|
org_rows = await sor.R('organization', {'parentid': orgid, 'del_flg': '0'})
|
||||||
|
if customerid:
|
||||||
|
org_rows = [row for row in org_rows if row.get('id') == customerid]
|
||||||
|
org_map = {row['id']: row for row in org_rows}
|
||||||
|
|
||||||
|
user_map = {}
|
||||||
|
for oid in org_map:
|
||||||
|
user_rows = await sor.R('users', {'orgid': oid, 'del_flg': '0'})
|
||||||
|
for user in user_rows:
|
||||||
|
user_map[user['id']] = user
|
||||||
|
return org_map, user_map
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_admin_summary(items, user_map, org_map):
|
||||||
|
buckets = {}
|
||||||
|
for item in items:
|
||||||
|
user = user_map.get(item.get('userid'), {})
|
||||||
|
org = org_map.get(user.get('orgid'), {})
|
||||||
|
model = item.get('model') or 'unknown'
|
||||||
|
key = (org.get('id'), item.get('userid'), model)
|
||||||
|
if key not in buckets:
|
||||||
|
buckets[key] = {
|
||||||
|
'customerid': org.get('id'),
|
||||||
|
'customer_name': org.get('orgname'),
|
||||||
|
'userid': item.get('userid'),
|
||||||
|
'username': user.get('username'),
|
||||||
|
'user_name': user.get('name'),
|
||||||
|
'model': model,
|
||||||
|
'prompt_tokens': 0,
|
||||||
|
'completion_tokens': 0,
|
||||||
|
'total_tokens': 0,
|
||||||
|
'amount': 0.0,
|
||||||
|
'request_count': 0,
|
||||||
|
'first_usage_time': item.get('usage_time'),
|
||||||
|
'last_usage_time': item.get('usage_time'),
|
||||||
|
}
|
||||||
|
bucket = buckets[key]
|
||||||
|
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
|
||||||
|
bucket['completion_tokens'] += item.get('completion_tokens') or 0
|
||||||
|
bucket['total_tokens'] += item.get('total_tokens') or 0
|
||||||
|
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
|
||||||
|
bucket['request_count'] += 1
|
||||||
|
usage_time = item.get('usage_time')
|
||||||
|
if usage_time:
|
||||||
|
bucket['last_usage_time'] = usage_time
|
||||||
|
if (
|
||||||
|
not bucket.get('first_usage_time')
|
||||||
|
or str(usage_time) < str(bucket['first_usage_time'])
|
||||||
|
):
|
||||||
|
bucket['first_usage_time'] = usage_time
|
||||||
|
return sorted(buckets.values(), key=lambda x: x['amount'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_items(items, group_by=None):
|
||||||
|
if not group_by:
|
||||||
|
return items
|
||||||
|
|
||||||
|
buckets = {}
|
||||||
|
for item in items:
|
||||||
|
usage_time = item.get('usage_time')
|
||||||
|
if isinstance(usage_time, str):
|
||||||
|
dt = _parse_datetime(usage_time)
|
||||||
|
else:
|
||||||
|
dt = usage_time
|
||||||
|
if not dt:
|
||||||
|
key = 'unknown'
|
||||||
|
else:
|
||||||
|
key = _group_key(dt, group_by)
|
||||||
|
|
||||||
|
if key not in buckets:
|
||||||
|
buckets[key] = {
|
||||||
|
'period': key,
|
||||||
|
'model': item.get('model'),
|
||||||
|
'prompt_tokens': 0,
|
||||||
|
'completion_tokens': 0,
|
||||||
|
'total_tokens': 0,
|
||||||
|
'amount': 0.0,
|
||||||
|
'request_count': 0,
|
||||||
|
}
|
||||||
|
bucket = buckets[key]
|
||||||
|
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
|
||||||
|
bucket['completion_tokens'] += item.get('completion_tokens') or 0
|
||||||
|
bucket['total_tokens'] += item.get('total_tokens') or 0
|
||||||
|
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
|
||||||
|
bucket['request_count'] += 1
|
||||||
|
|
||||||
|
return sorted(buckets.values(), key=lambda x: x['period'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize(items):
|
||||||
|
return {
|
||||||
|
'request_count': len(items),
|
||||||
|
'prompt_tokens': sum(i.get('prompt_tokens') or 0 for i in items),
|
||||||
|
'completion_tokens': sum(i.get('completion_tokens') or 0 for i in items),
|
||||||
|
'total_tokens': sum(i.get('total_tokens') or 0 for i in items),
|
||||||
|
'amount': round(sum(float(i.get('amount') or 0) for i in items), 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def model_usage_user_report(ns={}):
|
||||||
|
"""
|
||||||
|
用户查看自己的模型使用记录。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
userid 可选,默认当前登录用户
|
||||||
|
start_time 开始时间,格式 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS
|
||||||
|
end_time 结束时间
|
||||||
|
range 快捷范围:hour / day / week(未传 start/end 时生效,默认 day)
|
||||||
|
model 按模型标识筛选(usage_content.model)
|
||||||
|
group_by 聚合粒度:hour / day / week,不传则返回明细
|
||||||
|
current_page 页码,默认 1
|
||||||
|
page_size 每页条数,默认 20
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
model, prompt_tokens, completion_tokens, total_tokens, amount, usage_time
|
||||||
|
"""
|
||||||
|
if ns.get('userid'):
|
||||||
|
userid = ns.get('userid')
|
||||||
|
else:
|
||||||
|
userid = await get_user()
|
||||||
|
if not userid:
|
||||||
|
server_error(401)
|
||||||
|
|
||||||
|
start_dt, end_dt, err = _resolve_time_range(ns)
|
||||||
|
if err:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
model_filter = ns.get('model')
|
||||||
|
group_by = (ns.get('group_by') or '').lower() or None
|
||||||
|
if group_by and group_by not in ('hour', 'day', 'week'):
|
||||||
|
return {'status': False, 'msg': 'group_by 仅支持 hour / day / week'}
|
||||||
|
|
||||||
|
page_size = int(ns.get('page_size', 20))
|
||||||
|
current_page = int(ns.get('current_page', 1))
|
||||||
|
offset = (current_page - 1) * page_size
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
user_rows = await sor.R('users', {'id': userid, 'del_flg': '0'})
|
||||||
|
if not user_rows:
|
||||||
|
return {'status': False, 'msg': '用户不存在'}
|
||||||
|
|
||||||
|
conditions = _build_usage_where(
|
||||||
|
userid=userid,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
model_filter=model_filter,
|
||||||
|
)
|
||||||
|
total_count = await _count_model_usage(sor, conditions)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
grouped = _aggregate_items(all_items, group_by)
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'userid': userid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': grouped,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
page_rows = await _query_model_usage_rows(
|
||||||
|
sor, conditions, limit=page_size, offset=offset,
|
||||||
|
)
|
||||||
|
items = await _enrich_usage_rows(sor, page_rows)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'userid': userid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'total_count': total_count,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': False, 'msg': '查询失败, %s' % str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def model_usage_admin_report(ns={}):
|
||||||
|
"""
|
||||||
|
管理员查看当前机构下所有客户的模型使用汇总。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
userid 可选,默认当前登录用户(须为机构管理员)
|
||||||
|
start_time 开始时间
|
||||||
|
end_time 结束时间
|
||||||
|
range 快捷范围:hour / day / week(未传 start/end 时生效,默认 day)
|
||||||
|
customerid 可选,按客户机构 id 筛选
|
||||||
|
model 可选,按模型标识筛选
|
||||||
|
group_by 聚合粒度:hour / day / week;不传则按客户+模型汇总
|
||||||
|
current_page 页码,默认 1
|
||||||
|
page_size 每页条数,默认 20
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
customerid, customer_name, userid, username, model,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, amount, usage_time
|
||||||
|
"""
|
||||||
|
if ns.get('userid'):
|
||||||
|
userid = ns.get('userid')
|
||||||
|
else:
|
||||||
|
userid = await get_user()
|
||||||
|
if not userid:
|
||||||
|
server_error(401)
|
||||||
|
|
||||||
|
start_dt, end_dt, err = _resolve_time_range(ns)
|
||||||
|
if err:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
customerid = ns.get('customerid')
|
||||||
|
model_filter = ns.get('model')
|
||||||
|
group_by = (ns.get('group_by') or '').lower() or None
|
||||||
|
if group_by and group_by not in ('hour', 'day', 'week'):
|
||||||
|
return {'status': False, 'msg': 'group_by 仅支持 hour / day / week'}
|
||||||
|
|
||||||
|
page_size = int(ns.get('page_size', 20))
|
||||||
|
current_page = int(ns.get('current_page', 1))
|
||||||
|
offset = (current_page - 1) * page_size
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
user_rows = await sor.R('users', {'id': userid, 'del_flg': '0'})
|
||||||
|
if not user_rows:
|
||||||
|
return {'status': False, 'msg': '用户不存在'}
|
||||||
|
|
||||||
|
orgid = user_rows[0].get('orgid')
|
||||||
|
user_role = await get_user_role({'userid': userid, 'sor': sor})
|
||||||
|
if user_role not in ('管理员', '运营', '运营管理员'):
|
||||||
|
return {'status': False, 'msg': '无权限,仅机构管理员可查看'}
|
||||||
|
|
||||||
|
org_map, user_map = await _fetch_customer_users(sor, orgid, customerid)
|
||||||
|
user_ids = list(user_map.keys())
|
||||||
|
if not user_ids:
|
||||||
|
empty_data = {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize([]),
|
||||||
|
'total_count': 0,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': [],
|
||||||
|
}
|
||||||
|
if group_by:
|
||||||
|
empty_data = {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize([]),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': [],
|
||||||
|
}
|
||||||
|
return {'status': True, 'msg': '查询成功', 'data': empty_data}
|
||||||
|
|
||||||
|
conditions = _build_usage_where(
|
||||||
|
userids=user_ids,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
model_filter=model_filter,
|
||||||
|
)
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
enriched_items = []
|
||||||
|
for item in all_items:
|
||||||
|
user = user_map.get(item.get('userid'), {})
|
||||||
|
org = org_map.get(user.get('orgid'), {})
|
||||||
|
enriched = dict(item)
|
||||||
|
enriched['customerid'] = org.get('id')
|
||||||
|
enriched['customer_name'] = org.get('orgname')
|
||||||
|
enriched['username'] = user.get('username')
|
||||||
|
enriched['user_name'] = user.get('name')
|
||||||
|
enriched_items.append(enriched)
|
||||||
|
grouped = _aggregate_items(enriched_items, group_by)
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': grouped,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_items = _aggregate_admin_summary(all_items, user_map, org_map)
|
||||||
|
total_count = len(summary_items)
|
||||||
|
page_items = summary_items[offset:offset + page_size]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'total_count': total_count,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': page_items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': False, 'msg': '查询失败, %s' % str(e)}
|
||||||
|
|
||||||
|
ret = await model_usage_admin_report(params_kw)
|
||||||
|
return ret
|
||||||
512
b/cntoai/model_usage_user_report.dspy
Normal file
512
b/cntoai/model_usage_user_report.dspy
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
def _escape(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return str(value).replace("'", "''")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_usage_content(raw):
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
return raw
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_time_range(ns):
|
||||||
|
"""
|
||||||
|
解析时间范围。
|
||||||
|
|
||||||
|
优先使用 start_time + end_time;否则按 range 快捷窗口:
|
||||||
|
hour -> 最近 1 小时
|
||||||
|
day -> 最近 1 天
|
||||||
|
week -> 最近 7 天
|
||||||
|
默认 day。
|
||||||
|
"""
|
||||||
|
start_time = ns.get('start_time')
|
||||||
|
end_time = ns.get('end_time')
|
||||||
|
range_type = (ns.get('range') or ns.get('range_type') or 'day').lower()
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if start_time and end_time:
|
||||||
|
start_dt = _parse_datetime(start_time)
|
||||||
|
end_dt = _parse_datetime(end_time, end_of_day=True)
|
||||||
|
if not start_dt or not end_dt:
|
||||||
|
return None, None, 'start_time / end_time 格式无效,请使用 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS'
|
||||||
|
if start_dt > end_dt:
|
||||||
|
return None, None, 'start_time 不能晚于 end_time'
|
||||||
|
return start_dt, end_dt, None
|
||||||
|
|
||||||
|
delta_map = {
|
||||||
|
'hour': datetime.timedelta(hours=1),
|
||||||
|
'day': datetime.timedelta(days=1),
|
||||||
|
'week': datetime.timedelta(weeks=1),
|
||||||
|
}
|
||||||
|
if range_type not in delta_map:
|
||||||
|
return None, None, 'range 仅支持 hour / day / week'
|
||||||
|
|
||||||
|
end_dt = now
|
||||||
|
start_dt = now - delta_map[range_type]
|
||||||
|
return start_dt, end_dt, None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_datetime(value, end_of_day=False):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
text = str(value).strip()
|
||||||
|
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d'):
|
||||||
|
try:
|
||||||
|
dt = datetime.datetime.strptime(text, fmt)
|
||||||
|
if fmt == '%Y-%m-%d' and end_of_day:
|
||||||
|
dt = dt.replace(hour=23, minute=59, second=59)
|
||||||
|
return dt
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_datetime(dt):
|
||||||
|
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
|
||||||
|
def _group_key(dt, group_by):
|
||||||
|
if group_by == 'hour':
|
||||||
|
return dt.strftime('%Y-%m-%d %H:00:00')
|
||||||
|
if group_by == 'week':
|
||||||
|
monday = dt - datetime.timedelta(days=dt.weekday())
|
||||||
|
return monday.strftime('%Y-%m-%d')
|
||||||
|
return dt.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_usage_row(row, bill_amount_map=None):
|
||||||
|
usage = _parse_usage_content(row.get('usage_content'))
|
||||||
|
orderid = row.get('orderid')
|
||||||
|
amount = float(row.get('original_price') or 0)
|
||||||
|
if bill_amount_map and orderid and orderid in bill_amount_map:
|
||||||
|
amount = float(bill_amount_map[orderid] or amount)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': row.get('id'),
|
||||||
|
'userid': row.get('userid'),
|
||||||
|
'llmid': row.get('llmid'),
|
||||||
|
'model': usage.get('model') or row.get('model_name') or row.get('llm_model'),
|
||||||
|
'prompt_tokens': int(usage.get('prompt_tokens') or 0),
|
||||||
|
'completion_tokens': int(usage.get('completion_tokens') or 0),
|
||||||
|
'total_tokens': int(usage.get('total_tokens') or 0),
|
||||||
|
'amount': round(amount, 8),
|
||||||
|
'bill_status': row.get('bill_status'),
|
||||||
|
'orderid': orderid,
|
||||||
|
'usage_time': row.get('created_at'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_bill_amount_map(sor, order_ids):
|
||||||
|
if not order_ids:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
for orderid in set(order_ids):
|
||||||
|
if not orderid:
|
||||||
|
continue
|
||||||
|
rows = await sor.R('bill', {'orderid': orderid, 'del_flg': '0'})
|
||||||
|
if rows:
|
||||||
|
result[orderid] = sum(float(r.get('amount') or 0) for r in rows)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_llm_model_map(sor, llm_ids):
|
||||||
|
result = {}
|
||||||
|
for llmid in set(llm_ids):
|
||||||
|
if not llmid:
|
||||||
|
continue
|
||||||
|
rows = await sor.R('llm', {'id': llmid})
|
||||||
|
if rows:
|
||||||
|
result[llmid] = rows[0].get('model')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _build_usage_where(userid=None, userids=None, start_dt=None, end_dt=None, model_filter=None):
|
||||||
|
"""构建 model_usage 单表查询条件(不使用 JOIN,兼容 sqlExe 环境)。"""
|
||||||
|
conditions = []
|
||||||
|
if userid:
|
||||||
|
conditions.append("userid = '%s'" % _escape(userid))
|
||||||
|
elif userids:
|
||||||
|
escaped = ["'%s'" % _escape(uid) for uid in userids if uid]
|
||||||
|
if not escaped:
|
||||||
|
conditions.append('1 = 0')
|
||||||
|
else:
|
||||||
|
conditions.append('userid IN (%s)' % ','.join(escaped))
|
||||||
|
if start_dt and end_dt:
|
||||||
|
conditions.append(
|
||||||
|
"created_at BETWEEN '%s' AND '%s'"
|
||||||
|
% (_format_datetime(start_dt), _format_datetime(end_dt))
|
||||||
|
)
|
||||||
|
if model_filter:
|
||||||
|
conditions.append(
|
||||||
|
"JSON_UNQUOTE(JSON_EXTRACT(usage_content, '$.model')) = '%s'"
|
||||||
|
% _escape(model_filter)
|
||||||
|
)
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
|
async def _query_model_usage_rows(sor, conditions, limit=None, offset=None):
|
||||||
|
where_clause = ' AND '.join(conditions) if conditions else '1 = 1'
|
||||||
|
sql = """
|
||||||
|
SELECT id, userid, llmid, original_price, orderid, bill_status, usage_content, created_at
|
||||||
|
FROM model_usage
|
||||||
|
WHERE %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
""" % where_clause
|
||||||
|
if limit is not None:
|
||||||
|
sql += ' LIMIT %d OFFSET %d' % (int(limit), int(offset or 0))
|
||||||
|
return await sor.sqlExe(sql, {})
|
||||||
|
|
||||||
|
|
||||||
|
async def _count_model_usage(sor, conditions):
|
||||||
|
where_clause = ' AND '.join(conditions) if conditions else '1 = 1'
|
||||||
|
sql = 'SELECT COUNT(*) AS total_count FROM model_usage WHERE %s' % where_clause
|
||||||
|
return (await sor.sqlExe(sql, {}))[0]['total_count']
|
||||||
|
|
||||||
|
|
||||||
|
async def _enrich_usage_rows(sor, rows):
|
||||||
|
order_ids = [row.get('orderid') for row in rows if row.get('orderid')]
|
||||||
|
llm_ids = [row.get('llmid') for row in rows if row.get('llmid')]
|
||||||
|
bill_amount_map = await _fetch_bill_amount_map(sor, order_ids)
|
||||||
|
llm_model_map = await _fetch_llm_model_map(sor, llm_ids)
|
||||||
|
items = []
|
||||||
|
for row in rows:
|
||||||
|
enriched = dict(row)
|
||||||
|
enriched['llm_model'] = llm_model_map.get(row.get('llmid'))
|
||||||
|
items.append(_normalize_usage_row(enriched, bill_amount_map))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_customer_users(sor, orgid, customerid=None):
|
||||||
|
"""获取机构下客户及其用户映射。"""
|
||||||
|
org_rows = await sor.R('organization', {'parentid': orgid, 'del_flg': '0'})
|
||||||
|
if customerid:
|
||||||
|
org_rows = [row for row in org_rows if row.get('id') == customerid]
|
||||||
|
org_map = {row['id']: row for row in org_rows}
|
||||||
|
|
||||||
|
user_map = {}
|
||||||
|
for oid in org_map:
|
||||||
|
user_rows = await sor.R('users', {'orgid': oid, 'del_flg': '0'})
|
||||||
|
for user in user_rows:
|
||||||
|
user_map[user['id']] = user
|
||||||
|
return org_map, user_map
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_admin_summary(items, user_map, org_map):
|
||||||
|
buckets = {}
|
||||||
|
for item in items:
|
||||||
|
user = user_map.get(item.get('userid'), {})
|
||||||
|
org = org_map.get(user.get('orgid'), {})
|
||||||
|
model = item.get('model') or 'unknown'
|
||||||
|
key = (org.get('id'), item.get('userid'), model)
|
||||||
|
if key not in buckets:
|
||||||
|
buckets[key] = {
|
||||||
|
'customerid': org.get('id'),
|
||||||
|
'customer_name': org.get('orgname'),
|
||||||
|
'userid': item.get('userid'),
|
||||||
|
'username': user.get('username'),
|
||||||
|
'user_name': user.get('name'),
|
||||||
|
'model': model,
|
||||||
|
'prompt_tokens': 0,
|
||||||
|
'completion_tokens': 0,
|
||||||
|
'total_tokens': 0,
|
||||||
|
'amount': 0.0,
|
||||||
|
'request_count': 0,
|
||||||
|
'first_usage_time': item.get('usage_time'),
|
||||||
|
'last_usage_time': item.get('usage_time'),
|
||||||
|
}
|
||||||
|
bucket = buckets[key]
|
||||||
|
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
|
||||||
|
bucket['completion_tokens'] += item.get('completion_tokens') or 0
|
||||||
|
bucket['total_tokens'] += item.get('total_tokens') or 0
|
||||||
|
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
|
||||||
|
bucket['request_count'] += 1
|
||||||
|
usage_time = item.get('usage_time')
|
||||||
|
if usage_time:
|
||||||
|
bucket['last_usage_time'] = usage_time
|
||||||
|
if (
|
||||||
|
not bucket.get('first_usage_time')
|
||||||
|
or str(usage_time) < str(bucket['first_usage_time'])
|
||||||
|
):
|
||||||
|
bucket['first_usage_time'] = usage_time
|
||||||
|
return sorted(buckets.values(), key=lambda x: x['amount'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_items(items, group_by=None):
|
||||||
|
if not group_by:
|
||||||
|
return items
|
||||||
|
|
||||||
|
buckets = {}
|
||||||
|
for item in items:
|
||||||
|
usage_time = item.get('usage_time')
|
||||||
|
if isinstance(usage_time, str):
|
||||||
|
dt = _parse_datetime(usage_time)
|
||||||
|
else:
|
||||||
|
dt = usage_time
|
||||||
|
if not dt:
|
||||||
|
key = 'unknown'
|
||||||
|
else:
|
||||||
|
key = _group_key(dt, group_by)
|
||||||
|
|
||||||
|
if key not in buckets:
|
||||||
|
buckets[key] = {
|
||||||
|
'period': key,
|
||||||
|
'model': item.get('model'),
|
||||||
|
'prompt_tokens': 0,
|
||||||
|
'completion_tokens': 0,
|
||||||
|
'total_tokens': 0,
|
||||||
|
'amount': 0.0,
|
||||||
|
'request_count': 0,
|
||||||
|
}
|
||||||
|
bucket = buckets[key]
|
||||||
|
bucket['prompt_tokens'] += item.get('prompt_tokens') or 0
|
||||||
|
bucket['completion_tokens'] += item.get('completion_tokens') or 0
|
||||||
|
bucket['total_tokens'] += item.get('total_tokens') or 0
|
||||||
|
bucket['amount'] = round(bucket['amount'] + float(item.get('amount') or 0), 8)
|
||||||
|
bucket['request_count'] += 1
|
||||||
|
|
||||||
|
return sorted(buckets.values(), key=lambda x: x['period'], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _summarize(items):
|
||||||
|
return {
|
||||||
|
'request_count': len(items),
|
||||||
|
'prompt_tokens': sum(i.get('prompt_tokens') or 0 for i in items),
|
||||||
|
'completion_tokens': sum(i.get('completion_tokens') or 0 for i in items),
|
||||||
|
'total_tokens': sum(i.get('total_tokens') or 0 for i in items),
|
||||||
|
'amount': round(sum(float(i.get('amount') or 0) for i in items), 8),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def model_usage_user_report(ns={}):
|
||||||
|
"""
|
||||||
|
用户查看自己的模型使用记录。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
userid 可选,默认当前登录用户
|
||||||
|
start_time 开始时间,格式 YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS
|
||||||
|
end_time 结束时间
|
||||||
|
range 快捷范围:hour / day / week(未传 start/end 时生效,默认 day)
|
||||||
|
model 按模型标识筛选(usage_content.model)
|
||||||
|
group_by 聚合粒度:hour / day / week,不传则返回明细
|
||||||
|
current_page 页码,默认 1
|
||||||
|
page_size 每页条数,默认 20
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
model, prompt_tokens, completion_tokens, total_tokens, amount, usage_time
|
||||||
|
"""
|
||||||
|
if ns.get('userid'):
|
||||||
|
userid = ns.get('userid')
|
||||||
|
else:
|
||||||
|
userid = await get_user()
|
||||||
|
if not userid:
|
||||||
|
server_error(401)
|
||||||
|
|
||||||
|
start_dt, end_dt, err = _resolve_time_range(ns)
|
||||||
|
if err:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
model_filter = ns.get('model')
|
||||||
|
group_by = (ns.get('group_by') or '').lower() or None
|
||||||
|
if group_by and group_by not in ('hour', 'day', 'week'):
|
||||||
|
return {'status': False, 'msg': 'group_by 仅支持 hour / day / week'}
|
||||||
|
|
||||||
|
page_size = int(ns.get('page_size', 20))
|
||||||
|
current_page = int(ns.get('current_page', 1))
|
||||||
|
offset = (current_page - 1) * page_size
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
user_rows = await sor.R('users', {'id': userid, 'del_flg': '0'})
|
||||||
|
if not user_rows:
|
||||||
|
return {'status': False, 'msg': '用户不存在'}
|
||||||
|
|
||||||
|
conditions = _build_usage_where(
|
||||||
|
userid=userid,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
model_filter=model_filter,
|
||||||
|
)
|
||||||
|
total_count = await _count_model_usage(sor, conditions)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
grouped = _aggregate_items(all_items, group_by)
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'userid': userid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': grouped,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
page_rows = await _query_model_usage_rows(
|
||||||
|
sor, conditions, limit=page_size, offset=offset,
|
||||||
|
)
|
||||||
|
items = await _enrich_usage_rows(sor, page_rows)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'userid': userid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'total_count': total_count,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': False, 'msg': '查询失败, %s' % str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def model_usage_admin_report(ns={}):
|
||||||
|
"""
|
||||||
|
管理员查看当前机构下所有客户的模型使用汇总。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
userid 可选,默认当前登录用户(须为机构管理员)
|
||||||
|
start_time 开始时间
|
||||||
|
end_time 结束时间
|
||||||
|
range 快捷范围:hour / day / week(未传 start/end 时生效,默认 day)
|
||||||
|
customerid 可选,按客户机构 id 筛选
|
||||||
|
model 可选,按模型标识筛选
|
||||||
|
group_by 聚合粒度:hour / day / week;不传则按客户+模型汇总
|
||||||
|
current_page 页码,默认 1
|
||||||
|
page_size 每页条数,默认 20
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
customerid, customer_name, userid, username, model,
|
||||||
|
prompt_tokens, completion_tokens, total_tokens, amount, usage_time
|
||||||
|
"""
|
||||||
|
if ns.get('userid'):
|
||||||
|
userid = ns.get('userid')
|
||||||
|
else:
|
||||||
|
userid = await get_user()
|
||||||
|
if not userid:
|
||||||
|
server_error(401)
|
||||||
|
|
||||||
|
start_dt, end_dt, err = _resolve_time_range(ns)
|
||||||
|
if err:
|
||||||
|
return {'status': False, 'msg': err}
|
||||||
|
|
||||||
|
customerid = ns.get('customerid')
|
||||||
|
model_filter = ns.get('model')
|
||||||
|
group_by = (ns.get('group_by') or '').lower() or None
|
||||||
|
if group_by and group_by not in ('hour', 'day', 'week'):
|
||||||
|
return {'status': False, 'msg': 'group_by 仅支持 hour / day / week'}
|
||||||
|
|
||||||
|
page_size = int(ns.get('page_size', 20))
|
||||||
|
current_page = int(ns.get('current_page', 1))
|
||||||
|
offset = (current_page - 1) * page_size
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
user_rows = await sor.R('users', {'id': userid, 'del_flg': '0'})
|
||||||
|
if not user_rows:
|
||||||
|
return {'status': False, 'msg': '用户不存在'}
|
||||||
|
|
||||||
|
orgid = user_rows[0].get('orgid')
|
||||||
|
user_role = await get_user_role({'userid': userid, 'sor': sor})
|
||||||
|
if user_role not in ('管理员', '运营', '运营管理员'):
|
||||||
|
return {'status': False, 'msg': '无权限,仅机构管理员可查看'}
|
||||||
|
|
||||||
|
org_map, user_map = await _fetch_customer_users(sor, orgid, customerid)
|
||||||
|
user_ids = list(user_map.keys())
|
||||||
|
if not user_ids:
|
||||||
|
empty_data = {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize([]),
|
||||||
|
'total_count': 0,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': [],
|
||||||
|
}
|
||||||
|
if group_by:
|
||||||
|
empty_data = {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize([]),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': [],
|
||||||
|
}
|
||||||
|
return {'status': True, 'msg': '查询成功', 'data': empty_data}
|
||||||
|
|
||||||
|
conditions = _build_usage_where(
|
||||||
|
userids=user_ids,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
model_filter=model_filter,
|
||||||
|
)
|
||||||
|
all_rows = await _query_model_usage_rows(sor, conditions)
|
||||||
|
all_items = await _enrich_usage_rows(sor, all_rows)
|
||||||
|
|
||||||
|
if group_by:
|
||||||
|
enriched_items = []
|
||||||
|
for item in all_items:
|
||||||
|
user = user_map.get(item.get('userid'), {})
|
||||||
|
org = org_map.get(user.get('orgid'), {})
|
||||||
|
enriched = dict(item)
|
||||||
|
enriched['customerid'] = org.get('id')
|
||||||
|
enriched['customer_name'] = org.get('orgname')
|
||||||
|
enriched['username'] = user.get('username')
|
||||||
|
enriched['user_name'] = user.get('name')
|
||||||
|
enriched_items.append(enriched)
|
||||||
|
grouped = _aggregate_items(enriched_items, group_by)
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'group_by': group_by,
|
||||||
|
'groups': grouped,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_items = _aggregate_admin_summary(all_items, user_map, org_map)
|
||||||
|
total_count = len(summary_items)
|
||||||
|
page_items = summary_items[offset:offset + page_size]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': True,
|
||||||
|
'msg': '查询成功',
|
||||||
|
'data': {
|
||||||
|
'orgid': orgid,
|
||||||
|
'start_time': _format_datetime(start_dt),
|
||||||
|
'end_time': _format_datetime(end_dt),
|
||||||
|
'summary': _summarize(all_items),
|
||||||
|
'total_count': total_count,
|
||||||
|
'current_page': current_page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'items': page_items,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': False, 'msg': '查询失败, %s' % str(e)}
|
||||||
|
|
||||||
|
ret = await model_usage_user_report(params_kw)
|
||||||
|
return ret
|
||||||
@ -231,9 +231,10 @@ async def process_user_billing(ns={}):
|
|||||||
:param quantity: 仅 use_saleprotocol=True 时生效,数量默认 1
|
:param quantity: 仅 use_saleprotocol=True 时生效,数量默认 1
|
||||||
:return: dict,含 status、msg;成功时含 orderid、amount
|
:return: dict,含 status、msg;成功时含 orderid、amount
|
||||||
"""
|
"""
|
||||||
|
debug(f"process_user_billing 传递参数: {ns}")
|
||||||
# 存储输入值到usage表
|
# 存储输入值到usage表
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext('kboss') as sor:
|
async with db.sqlorContext('kboss') as sor_init:
|
||||||
usage_ns = {
|
usage_ns = {
|
||||||
'id': uuid(),
|
'id': uuid(),
|
||||||
'userid': ns.get('userid'),
|
'userid': ns.get('userid'),
|
||||||
@ -242,7 +243,7 @@ async def process_user_billing(ns={}):
|
|||||||
'original_price': ns.get('amount'),
|
'original_price': ns.get('amount'),
|
||||||
'usage_content': json.dumps(ns.get('usage')) if isinstance(ns.get('usage'), dict) else ns.get('usage')
|
'usage_content': json.dumps(ns.get('usage')) if isinstance(ns.get('usage'), dict) else ns.get('usage')
|
||||||
}
|
}
|
||||||
await sor.C('model_usage', usage_ns)
|
await sor_init.C('model_usage', usage_ns)
|
||||||
|
|
||||||
apikey = ns.get('apikey')
|
apikey = ns.get('apikey')
|
||||||
userid = ns.get('userid')
|
userid = ns.get('userid')
|
||||||
@ -328,7 +329,7 @@ async def process_user_billing(ns={}):
|
|||||||
price_res = await calc_price_by_saleprotocol(
|
price_res = await calc_price_by_saleprotocol(
|
||||||
sor, org_list[0], product['id'], amount, quantity=quantity,
|
sor, org_list[0], product['id'], amount, quantity=quantity,
|
||||||
)
|
)
|
||||||
if not price_res['status']:
|
if price_res['status'] == 'error':
|
||||||
return price_res
|
return price_res
|
||||||
debug(price_res)
|
debug(price_res)
|
||||||
debug('list_price %s' % list_price)
|
debug('list_price %s' % list_price)
|
||||||
@ -386,7 +387,7 @@ async def process_user_billing(ns={}):
|
|||||||
await sor.C('order_goods', goods_ns)
|
await sor.C('order_goods', goods_ns)
|
||||||
|
|
||||||
charge_res = await _charge_order(sor, order_id, order_type='NEW')
|
charge_res = await _charge_order(sor, order_id, order_type='NEW')
|
||||||
if not charge_res['status']:
|
if charge_res['status'] == 'error':
|
||||||
await sor.rollback()
|
await sor.rollback()
|
||||||
return charge_res
|
return charge_res
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,14 @@ async def getNoinvitationcode(ns={}):
|
|||||||
try:
|
try:
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
async with db.sqlorContext('kboss') as sor:
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
userid = ns.get('userid')
|
||||||
|
if not userid:
|
||||||
|
userid = await get_user()
|
||||||
|
userinfo_li = await sor.R('users', {'id': userid})
|
||||||
|
if userinfo_li:
|
||||||
|
orgid = userinfo_li[0]['orgid']
|
||||||
|
ns['orgid'] = orgid
|
||||||
|
|
||||||
# 获取组织信息
|
# 获取组织信息
|
||||||
orgs = await sor.R('organization', {"org_type": '0'})
|
orgs = await sor.R('organization', {"org_type": '0'})
|
||||||
if not orgs:
|
if not orgs:
|
||||||
@ -69,6 +77,7 @@ async def getNoinvitationcode(ns={}):
|
|||||||
data_sql = data_sql % tuple(params)
|
data_sql = data_sql % tuple(params)
|
||||||
|
|
||||||
# 执行数据查询
|
# 执行数据查询
|
||||||
|
debug(f"getNoinvitationcode 查询数据: {data_sql}")
|
||||||
result = await sor.sqlExe(data_sql, {})
|
result = await sor.sqlExe(data_sql, {})
|
||||||
|
|
||||||
# 返回结果,包含分页信息
|
# 返回结果,包含分页信息
|
||||||
|
|||||||
@ -140,7 +140,8 @@ async def logintype(ns):
|
|||||||
async with db.sqlorContext('kboss') as sor:
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
|
||||||
domain_name = ns.get('domain_name')
|
domain_name = ns.get('domain_name')
|
||||||
if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527'] and ns.get('username') not in ['开元云(北京)科技有限公司', 'admin', 'kyy_root', 'kyy_kaiyuan', 'kyacloud', 'kyy_运营', 'kyy_销售', 'kyy_财务', '测试用户', 'kycloud']:
|
# if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527', 'www.ncmatch.cn'] and ns.get('username') not in ['开元云(北京)科技有限公司', 'admin', 'kyy_root', 'kyy_kaiyuan', 'kyacloud', 'kyy_运营', 'kyy_销售', 'kyy_财务', '测试用户', 'kycloud']:
|
||||||
|
if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527', 'www.ncmatch.cn'] and not ns.get('username') and not ns.get('codeid'):
|
||||||
|
|
||||||
# 登录失败次数限制
|
# 登录失败次数限制
|
||||||
login_allowed = await check_login_allowed(ns.get('username'))
|
login_allowed = await check_login_allowed(ns.get('username'))
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {getHomePath} from "@/views/setting/tools";
|
|||||||
|
|
||||||
NProgress.configure({showSpinner: false}); // NProgress Configuration
|
NProgress.configure({showSpinner: false}); // NProgress Configuration
|
||||||
|
|
||||||
const whiteList = ["product","/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist
|
const whiteList = ["product", "/tokenMarket", "/modelDetail", "/modelApiDocument", "/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist
|
||||||
|
|
||||||
// 获取用户代理字符串
|
// 获取用户代理字符串
|
||||||
const userAgent = window.navigator.userAgent;
|
const userAgent = window.navigator.userAgent;
|
||||||
|
|||||||
@ -62,6 +62,16 @@ export const constantRoutes = [
|
|||||||
component: () => import('@/views/modelProductDetail/index.vue'),
|
component: () => import('@/views/modelProductDetail/index.vue'),
|
||||||
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/tokenMarket",
|
||||||
|
name: "PublicTokenMarket",
|
||||||
|
component: () => import('@/views/product/allProduct/index.vue'),
|
||||||
|
hidden: true,
|
||||||
|
meta: {
|
||||||
|
title: "TOKEN市集",
|
||||||
|
noCache: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/h5HomePage',
|
path: '/h5HomePage',
|
||||||
name: 'H5HomePage',
|
name: 'H5HomePage',
|
||||||
@ -392,6 +402,28 @@ export const constantRoutes = [
|
|||||||
meta: { title: "立即支付页面" },
|
meta: { title: "立即支付页面" },
|
||||||
}, {
|
}, {
|
||||||
path: "/auth-redirect", component: () => import("@/views/login/auth-redirect"), hidden: true,
|
path: "/auth-redirect", component: () => import("@/views/login/auth-redirect"), hidden: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/modelDetail",
|
||||||
|
component: () => import('@/views/modelManagement/ModelDetail.vue'),
|
||||||
|
hidden: true,
|
||||||
|
name: 'modelDetail',
|
||||||
|
meta: {
|
||||||
|
title: "模型详情",
|
||||||
|
fullPath: "/modelDetail",
|
||||||
|
noCache: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/modelApiDocument",
|
||||||
|
component: () => import('@/views/modelManagement/ApiDocument.vue'),
|
||||||
|
hidden: true,
|
||||||
|
name: 'modelApiDocument',
|
||||||
|
meta: {
|
||||||
|
title: "API文档",
|
||||||
|
fullPath: "/modelApiDocument",
|
||||||
|
noCache: true
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
path: "/404", component: () => import("@/views/error-page/404"), hidden: true,
|
path: "/404", component: () => import("@/views/error-page/404"), hidden: true,
|
||||||
}, {
|
}, {
|
||||||
@ -480,6 +512,30 @@ export const asyncRoutes = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// Token用量 - 一级菜单(所有登录用户都能看到)
|
||||||
|
{
|
||||||
|
path: "/tokenUsage",
|
||||||
|
component: Layout,
|
||||||
|
meta: {
|
||||||
|
title: "Token用量",
|
||||||
|
fullPath: "/tokenUsage",
|
||||||
|
noCache: true,
|
||||||
|
icon: "el-icon-data-line"
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
component: () => import('@/views/tokenUsage/index.vue'),
|
||||||
|
name: 'TokenUsage',
|
||||||
|
meta: {
|
||||||
|
title: "Token用量",
|
||||||
|
fullPath: "/tokenUsage",
|
||||||
|
noCache: true,
|
||||||
|
icon: "el-icon-data-line"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
// 模型体验
|
// 模型体验
|
||||||
{
|
{
|
||||||
path: "/modelExperience",
|
path: "/modelExperience",
|
||||||
@ -492,30 +548,6 @@ export const asyncRoutes = [
|
|||||||
noCache: true
|
noCache: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 模型详情
|
|
||||||
{
|
|
||||||
path: "/modelDetail",
|
|
||||||
component: () => import('@/views/modelManagement/ModelDetail.vue'),
|
|
||||||
hidden: true,
|
|
||||||
name: 'modelDetail',
|
|
||||||
meta: {
|
|
||||||
title: "模型详情",
|
|
||||||
fullPath: "/modelDetail",
|
|
||||||
noCache: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// API文档
|
|
||||||
{
|
|
||||||
path: "/modelApiDocument",
|
|
||||||
component: () => import('@/views/modelManagement/ApiDocument.vue'),
|
|
||||||
hidden: true,
|
|
||||||
name: 'modelApiDocument',
|
|
||||||
meta: {
|
|
||||||
title: "API文档",
|
|
||||||
fullPath: "/modelApiDocument",
|
|
||||||
noCache: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/overview",
|
path: "/overview",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@ -1056,24 +1088,24 @@ export const asyncRoutes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 信息完善 - 变为一级菜单
|
// 信息完善 - 变为一级菜单
|
||||||
{
|
// {
|
||||||
path: "/informationPerfect",
|
// path: "/informationPerfect",
|
||||||
component: Layout,
|
// component: Layout,
|
||||||
meta: {
|
// meta: {
|
||||||
title: "信息完善",
|
// title: "信息完善",
|
||||||
icon: "el-icon-edit-outline",
|
// icon: "el-icon-edit-outline",
|
||||||
noCache: true,
|
// noCache: true,
|
||||||
fullPath: "/informationPerfect"
|
// fullPath: "/informationPerfect"
|
||||||
},
|
// },
|
||||||
children: [
|
// children: [
|
||||||
{
|
// {
|
||||||
path: "index",
|
// path: "index",
|
||||||
component: () => import('@/views/customer/ncApprove/index.vue'),
|
// component: () => import('@/views/customer/ncApprove/index.vue'),
|
||||||
name: "InformationPerfect",
|
// name: "InformationPerfect",
|
||||||
meta: { title: "信息完善", fullPath: "/informationPerfect/index" },
|
// meta: { title: "信息完善", fullPath: "/informationPerfect/index" },
|
||||||
}
|
// }
|
||||||
]
|
// ]
|
||||||
},
|
// },
|
||||||
// 充值管理 - 变为一级菜单
|
// 充值管理 - 变为一级菜单
|
||||||
{
|
{
|
||||||
path: "/rechargeManagement",
|
path: "/rechargeManagement",
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const SPECIAL_ORDER_USER = 'ZhipuHZ';
|
|||||||
const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator';
|
const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator';
|
||||||
|
|
||||||
// 所有登录用户都能访问的公共路由,不依赖后端 auths 和角色。hidden 路由不会显示在菜单里。
|
// 所有登录用户都能访问的公共路由,不依赖后端 auths 和角色。hidden 路由不会显示在菜单里。
|
||||||
const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/modelExperience', '/modelDetail', '/modelApiDocument'];
|
const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/tokenUsage', '/modelExperience', '/modelDetail', '/modelApiDocument'];
|
||||||
|
|
||||||
// 运营角色需要额外补出来的菜单。
|
// 运营角色需要额外补出来的菜单。
|
||||||
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/operationReport'];
|
const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/operationReport'];
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
<p @mouseleave="sildeOut" @mouseenter="sildeIn(product_service)">
|
<p @mouseleave="sildeOut" @mouseenter="sildeIn(product_service)">
|
||||||
<a>基础云</a>
|
<a>基础云</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="nav-hover" @click="handleModelSquareClick">token市集</p>
|
<p class="nav-hover" :class="{ active: isActiveTokenMarket }" @click="handleModelSquareClick">token市集</p>
|
||||||
<p class="nav-hover" @click="goYuanjing">元境</p>
|
<p class="nav-hover" @click="goYuanjing">元境</p>
|
||||||
<!-- 供需广场 -->
|
<!-- 供需广场 -->
|
||||||
<p :class="{ active: $route.path.includes('/supply') }">
|
<p :class="{ active: $route.path.includes('/supply') }">
|
||||||
@ -367,6 +367,11 @@ export default Vue.extend({
|
|||||||
return this.$route.path.includes('/homePage/index');
|
return this.$route.path.includes('/homePage/index');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isActiveTokenMarket() {
|
||||||
|
const category = this.$route.query.category || this.$route.query.tab || ''
|
||||||
|
return this.$route.path === '/tokenMarket' ||
|
||||||
|
(this.$route.path === '/product' && ['TOKEN市集', 'Token市集', 'token市集'].includes(category))
|
||||||
|
},
|
||||||
aiPanelStyle() {
|
aiPanelStyle() {
|
||||||
if (this.aiPanelPosition.left === null || this.aiPanelPosition.top === null) {
|
if (this.aiPanelPosition.left === null || this.aiPanelPosition.top === null) {
|
||||||
return {}
|
return {}
|
||||||
@ -381,19 +386,30 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 点击模型广场前校验登录状态
|
// 点击 TOKEN 市集:未登录也允许进入,但按单页面模式展示。
|
||||||
handleModelSquareClick() {
|
handleModelSquareClick() {
|
||||||
if (!this.loginState) {
|
if (!this.loginState) {
|
||||||
this.$message.warning('请先登录哦~')
|
this.$router.push({
|
||||||
|
path: '/tokenMarket',
|
||||||
|
query: {
|
||||||
|
category: 'TOKEN市集',
|
||||||
|
single: '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$router.push('/product')
|
this.$router.push({
|
||||||
|
path: '/product',
|
||||||
|
query: {
|
||||||
|
category: 'TOKEN市集'
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 跳转元境
|
// 跳转元境
|
||||||
async goYuanjing() {
|
async goYuanjing() {
|
||||||
if (!this.loginState) {
|
if (!this.loginState) {
|
||||||
this.$message.warning('请先登录哦~')
|
window.open('https://ai.opencomputing.cn/', '_blank')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1143,6 +1159,11 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.nav-hover.active {
|
||||||
|
color: #1E6FFF;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px !important;
|
font-size: 18px !important;
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div class="hero-inner">
|
<div class="hero-inner">
|
||||||
<p class="hero-slogan">好用还省钱,Token 就上开元云</p>
|
<p class="hero-slogan">好用还省钱,Token 就上开元云</p>
|
||||||
<h1>石景山<span>OPC</span>公共服务平台</h1>
|
<h1>数智开物<span>OPC</span>公共服务平台</h1>
|
||||||
<p class="hero-subtitle">
|
<p class="hero-subtitle">
|
||||||
为 <strong>OPC</strong> 而生,极致性价比一站式模型平台
|
为 <strong>OPC</strong> 而生,极致性价比一站式模型平台
|
||||||
</p>
|
</p>
|
||||||
@ -220,7 +220,14 @@ export default Vue.extend({
|
|||||||
methods: {
|
methods: {
|
||||||
goTokenMarket() {
|
goTokenMarket() {
|
||||||
if (!this.loginState) {
|
if (!this.loginState) {
|
||||||
this.$message.warning('请先登录哦~')
|
this.$router.push({
|
||||||
|
path: '/tokenMarket',
|
||||||
|
query: {
|
||||||
|
category: 'TOKEN市集',
|
||||||
|
from: 'home',
|
||||||
|
single: '1'
|
||||||
|
}
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
@ -264,7 +271,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
async goCreativeWorkshop() {
|
async goCreativeWorkshop() {
|
||||||
if (!this.loginState) {
|
if (!this.loginState) {
|
||||||
this.$message.warning('请先登录哦~')
|
window.open('https://ai.opencomputing.cn/', '_blank')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,10 +43,27 @@
|
|||||||
|
|
||||||
<!-- 登录容器 -->
|
<!-- 登录容器 -->
|
||||||
<div class="login-container login-form-style">
|
<div class="login-container login-form-style">
|
||||||
<el-form ref="loginForm" :model="loginForm" :rules="rules" class="login-form-l" autocomplete="on"
|
<div class="login-mode-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: loginMode === 'password' }"
|
||||||
|
@click="switchLoginMode('password')"
|
||||||
|
>
|
||||||
|
密码登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: loginMode === 'mobile' }"
|
||||||
|
@click="switchLoginMode('mobile')"
|
||||||
|
>
|
||||||
|
验证码登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form ref="loginForm" :model="loginForm" :rules="activeRules" class="login-form-l" autocomplete="on"
|
||||||
label-position="left">
|
label-position="left">
|
||||||
|
|
||||||
<el-form-item prop="username" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
<el-form-item v-if="loginMode === 'password'" prop="username" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<span class="svg-container icon-box">
|
<span class="svg-container icon-box">
|
||||||
<img src="../../assets/kyy/用户.svg" alt="">
|
<img src="../../assets/kyy/用户.svg" alt="">
|
||||||
@ -56,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-tooltip v-model="capsTooltip" style="margin-bottom: 7px" content="Caps lock is On" placement="right"
|
<el-tooltip v-if="loginMode === 'password'" v-model="capsTooltip" style="margin-bottom: 7px" content="Caps lock is On" placement="right"
|
||||||
manual>
|
manual>
|
||||||
<el-form-item prop="password" class="password-mobile"
|
<el-form-item prop="password" class="password-mobile"
|
||||||
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
@ -66,7 +83,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<el-input id="passwordId" :key="passwordType" ref="password" v-model="loginForm.password"
|
<el-input id="passwordId" :key="passwordType" ref="password" v-model="loginForm.password"
|
||||||
:type="passwordType" placeholder="请输入密码" name="password" @keyup.native="checkCapslock"
|
:type="passwordType" placeholder="请输入密码" name="password" @keyup.native="checkCapslock"
|
||||||
@blur="capsTooltip = false" @keyup.enter.native="handleLogin" />
|
@blur="capsTooltip = false" @keyup.enter.native="handleLogin('loginForm')" />
|
||||||
<span class="show-pwd" @click="showPwd">
|
<span class="show-pwd" @click="showPwd">
|
||||||
<svg-icon style="margin-right: 10px;cursor: pointer"
|
<svg-icon style="margin-right: 10px;cursor: pointer"
|
||||||
:icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
|
:icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
|
||||||
@ -75,7 +92,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
<el-form-item prop="mobile" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
<el-form-item v-if="loginMode === 'mobile'" prop="mobile" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<span class="svg-container icon-box">
|
<span class="svg-container icon-box">
|
||||||
<img src="../../assets/kyy/用户.svg" alt="">
|
<img src="../../assets/kyy/用户.svg" alt="">
|
||||||
@ -85,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<div style="display:flex;margin-top:20px;">
|
<div v-if="loginMode === 'mobile'" style="display:flex;margin-top:20px;">
|
||||||
<el-form-item prop="vcode" class="invitecode"
|
<el-form-item prop="vcode" class="invitecode"
|
||||||
style="background-color: white; border: 1px solid #d9d9d9;">
|
style="background-color: white; border: 1px solid #d9d9d9;">
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
@ -201,6 +218,7 @@ export default {
|
|||||||
// 对话框和加载状态
|
// 对话框和加载状态
|
||||||
loading: false, // 登录按钮加载状态
|
loading: false, // 登录按钮加载状态
|
||||||
forgotPasswordVisible: false, // 忘记密码弹窗显示状态
|
forgotPasswordVisible: false, // 忘记密码弹窗显示状态
|
||||||
|
loginMode: 'password', // 登录模式:password-账号密码,mobile-手机号验证码
|
||||||
|
|
||||||
// 登录表单数据
|
// 登录表单数据
|
||||||
loginForm: {
|
loginForm: {
|
||||||
@ -247,6 +265,19 @@ export default {
|
|||||||
// 检查当前域名是否为ncmatch.cn
|
// 检查当前域名是否为ncmatch.cn
|
||||||
isNcmatchDomain() {
|
isNcmatchDomain() {
|
||||||
return window.location.hostname.includes('ncmatch.cn');
|
return window.location.hostname.includes('ncmatch.cn');
|
||||||
|
},
|
||||||
|
activeRules() {
|
||||||
|
if (this.loginMode === 'mobile') {
|
||||||
|
return {
|
||||||
|
mobile: this.rules.mobile,
|
||||||
|
vcode: this.rules.vcode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
username: this.rules.username,
|
||||||
|
password: this.rules.password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -693,6 +724,16 @@ export default {
|
|||||||
console.log(tab, event);
|
console.log(tab, event);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 切换登录方式,并清理上一种登录方式的校验提示。
|
||||||
|
switchLoginMode(mode) {
|
||||||
|
if (this.loginMode === mode) return;
|
||||||
|
this.loginMode = mode;
|
||||||
|
this.capsTooltip = false;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.loginForm && this.$refs.loginForm.clearValidate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// 检查大写锁定状态
|
// 检查大写锁定状态
|
||||||
checkCapslock(e) {
|
checkCapslock(e) {
|
||||||
const { key } = e;
|
const { key } = e;
|
||||||
@ -703,15 +744,7 @@ export default {
|
|||||||
handleLogin(formName) {
|
handleLogin(formName) {
|
||||||
this.$refs[formName].validate((valid) => {
|
this.$refs[formName].validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
const loginParams = {
|
const loginParams = this.buildLoginParams()
|
||||||
username: this.loginForm.username,
|
|
||||||
domain_name: this.photosUrl?.domain_name ? this.photosUrl.domain_name : '',
|
|
||||||
password: this.passwordEncryption(this.loginForm.password),
|
|
||||||
mobile: this.loginForm.mobile,
|
|
||||||
vcode: this.loginForm.vcode,
|
|
||||||
codeid: this.loginForm.codeid,
|
|
||||||
wechat_openid: this.wechat_openid
|
|
||||||
}
|
|
||||||
|
|
||||||
logintypeAPI(loginParams).then(res => {
|
logintypeAPI(loginParams).then(res => {
|
||||||
if (res.status == true) {
|
if (res.status == true) {
|
||||||
@ -803,16 +836,38 @@ export default {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: res.msg,
|
message: "请完善登录信息",
|
||||||
|
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
this.$refs.loginForm.resetFields();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 根据当前登录模式生成提交参数,避免把另一种模式的空字段带给后端。
|
||||||
|
buildLoginParams() {
|
||||||
|
const commonParams = {
|
||||||
|
domain_name: this.photosUrl?.domain_name ? this.photosUrl.domain_name : '',
|
||||||
|
wechat_openid: this.wechat_openid
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.loginMode === 'mobile') {
|
||||||
|
return {
|
||||||
|
...commonParams,
|
||||||
|
mobile: this.loginForm.mobile,
|
||||||
|
vcode: this.loginForm.vcode,
|
||||||
|
codeid: this.loginForm.codeid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonParams,
|
||||||
|
username: this.loginForm.username,
|
||||||
|
password: this.passwordEncryption(this.loginForm.password)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
// 显示/隐藏密码
|
// 显示/隐藏密码
|
||||||
showPwd() {
|
showPwd() {
|
||||||
if (this.passwordType === "password") {
|
if (this.passwordType === "password") {
|
||||||
@ -838,7 +893,9 @@ export default {
|
|||||||
$cursor: black;
|
$cursor: black;
|
||||||
$light_gray: #eee;
|
$light_gray: #eee;
|
||||||
$dark_gray: #889aa4;
|
$dark_gray: #889aa4;
|
||||||
|
.el-form{
|
||||||
|
padding:20px 0;
|
||||||
|
}
|
||||||
.main-box {
|
.main-box {
|
||||||
background: url('/static/img/banner.0798e703.png') no-repeat center center;
|
background: url('/static/img/banner.0798e703.png') no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@ -886,24 +943,60 @@ $dark_gray: #889aa4;
|
|||||||
|
|
||||||
/* 统一登录表单容器 */
|
/* 统一登录表单容器 */
|
||||||
.login-form {
|
.login-form {
|
||||||
padding: 40px 50px;
|
width: 420px;
|
||||||
|
padding: 24px 60px 22px;
|
||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0 18px 46px rgba(34, 47, 96, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 登录标题统一间距 */
|
/* 登录标题统一间距 */
|
||||||
.login-text {
|
.login-text {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 12px;
|
||||||
font-family: PingFang SC;
|
font-family: PingFang SC;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-mode-tabs {
|
||||||
|
display: flex;
|
||||||
|
width: 300px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 16px 0;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d8e6ff;
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 36px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: all 0.22s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1e6fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #1e6fff, #4aa3ff);
|
||||||
|
box-shadow: 0 8px 18px rgba(30, 111, 255, 0.28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 统一输入框容器样式 */
|
/* 统一输入框容器样式 */
|
||||||
.user-input {
|
.user-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -925,7 +1018,7 @@ $dark_gray: #889aa4;
|
|||||||
|
|
||||||
/* 统一表单项目样式 + 边距 */
|
/* 统一表单项目样式 + 边距 */
|
||||||
::v-deep .el-form-item {
|
::v-deep .el-form-item {
|
||||||
margin-bottom: 24px !important;
|
margin-bottom: 15px !important;
|
||||||
/* 给错误提示预留空间 */
|
/* 给错误提示预留空间 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1010,8 +1103,8 @@ $dark_gray: #889aa4;
|
|||||||
height: 42px;
|
height: 42px;
|
||||||
line-height: 42px;
|
line-height: 42px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-top: 10px !important;
|
margin-top: 0 !important;
|
||||||
margin-bottom: 20px !important;
|
margin-bottom: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部按钮区域统一 */
|
/* 底部按钮区域统一 */
|
||||||
@ -1024,7 +1117,7 @@ $dark_gray: #889aa4;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 12px;
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@ -182,6 +182,10 @@ print(response.json())`
|
|||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
if (this.$route.query.from === 'tokenMarket') {
|
if (this.$route.query.from === 'tokenMarket') {
|
||||||
|
if (this.$route.query.single === '1') {
|
||||||
|
this.$router.push({ path: '/tokenMarket', query: { category: 'TOKEN市集', single: '1' } })
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$router.push({ path: '/product', query: { category: 'TOKEN市集' } })
|
this.$router.push({ path: '/product', query: { category: 'TOKEN市集' } })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -272,6 +272,10 @@ export default {
|
|||||||
return value !== undefined && value !== null && value !== '' && value !== '-'
|
return value !== undefined && value !== null && value !== '' && value !== '-'
|
||||||
},
|
},
|
||||||
goTokenMarket() {
|
goTokenMarket() {
|
||||||
|
if (this.$route.query.single === '1') {
|
||||||
|
this.$router.push({ path: '/tokenMarket', query: { category: 'TOKEN市集', single: '1' } })
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$router.push({ path: '/product', query: { category: 'TOKEN市集' } })
|
this.$router.push({ path: '/product', query: { category: 'TOKEN市集' } })
|
||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
@ -286,7 +290,8 @@ export default {
|
|||||||
id: this.$route.query.id,
|
id: this.$route.query.id,
|
||||||
model_id: this.$route.query.model_id || this.$route.query.id,
|
model_id: this.$route.query.model_id || this.$route.query.id,
|
||||||
from: 'tokenMarket',
|
from: 'tokenMarket',
|
||||||
category: 'TOKEN市集'
|
category: 'TOKEN市集',
|
||||||
|
single: this.$route.query.single
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goApiDocument() {
|
goApiDocument() {
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="product-service-page">
|
<div class="product-service-page" :class="{ 'single-product-page': isSinglePageMode }">
|
||||||
|
<TopBox v-if="isSinglePageMode" class="single-page-topbox" />
|
||||||
|
|
||||||
<!-- 产品分类导航 -->
|
<!-- 产品分类导航 -->
|
||||||
<div class="category-nav">
|
<div v-if="!isSinglePageMode" class="category-nav">
|
||||||
<div v-for="category in panelData"
|
<div v-for="category in panelData"
|
||||||
:key="category.firTitle"
|
:key="category.firTitle"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
@ -194,9 +195,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import { reqNavList, reqNewHomeSync, reqNewHomeFestival } from "@/api/newHome";
|
import { reqNavList, reqNewHomeSync, reqNewHomeFestival } from "@/api/newHome";
|
||||||
import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
|
import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
|
||||||
|
import TopBox from '@/views/homePage/components/topBox/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProductServicePage",
|
name: "ProductServicePage",
|
||||||
|
components: {
|
||||||
|
TopBox
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
panelData: [],
|
panelData: [],
|
||||||
@ -216,6 +221,9 @@ export default {
|
|||||||
isTokenMarketActive() {
|
isTokenMarketActive() {
|
||||||
return this.isTokenMarketCategory(this.activeCategory);
|
return this.isTokenMarketCategory(this.activeCategory);
|
||||||
},
|
},
|
||||||
|
isSinglePageMode() {
|
||||||
|
return this.$route.path === '/tokenMarket' || this.$route.query.single === '1' || this.$route.query.single === 'true';
|
||||||
|
},
|
||||||
currentSubcategories() {
|
currentSubcategories() {
|
||||||
if (!this.activeCategory || !this.panelData.length) return [];
|
if (!this.activeCategory || !this.panelData.length) return [];
|
||||||
const category = this.panelData.find(item => item.firTitle === this.activeCategory);
|
const category = this.panelData.find(item => item.firTitle === this.activeCategory);
|
||||||
@ -356,7 +364,8 @@ export default {
|
|||||||
model_id: model.id,
|
model_id: model.id,
|
||||||
llmid: model.llmid || model.model_name || model.id,
|
llmid: model.llmid || model.model_name || model.id,
|
||||||
from: 'tokenMarket',
|
from: 'tokenMarket',
|
||||||
category: 'TOKEN市集'
|
category: 'TOKEN市集',
|
||||||
|
single: this.isSinglePageMode ? '1' : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -370,13 +379,18 @@ export default {
|
|||||||
id: model.id,
|
id: model.id,
|
||||||
model_id: model.id,
|
model_id: model.id,
|
||||||
from: 'tokenMarket',
|
from: 'tokenMarket',
|
||||||
category: 'TOKEN市集'
|
category: 'TOKEN市集',
|
||||||
|
single: this.isSinglePageMode ? '1' : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// TOKEN 市集体验按钮:进入模型体验页。
|
// TOKEN 市集体验按钮:进入模型体验页。
|
||||||
goModelExperience(model) {
|
goModelExperience(model) {
|
||||||
|
if (!this.loginState) {
|
||||||
|
this.$message.warning('请先登录再进行体验哦')
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.cacheTokenMarketModel(model);
|
this.cacheTokenMarketModel(model);
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: 'modelExperience',
|
name: 'modelExperience',
|
||||||
@ -548,7 +562,7 @@ export default {
|
|||||||
async goYuanjing() {
|
async goYuanjing() {
|
||||||
const userId = sessionStorage.getItem('userId')
|
const userId = sessionStorage.getItem('userId')
|
||||||
if (!userId || userId === 'null' || userId === '') {
|
if (!userId || userId === 'null' || userId === '') {
|
||||||
this.$message.warning('请先登录哦~')
|
window.open('https://ai.opencomputing.cn/', '_blank')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,6 +792,30 @@ export default {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
min-height: calc(100vh - 100px);
|
min-height: calc(100vh - 100px);
|
||||||
|
|
||||||
|
&.single-product-page {
|
||||||
|
height: 100vh;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28px;
|
||||||
|
background: linear-gradient(180deg, #f3f7ff 0%, #ffffff 100%);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.single-page-topbox {
|
||||||
|
width: 100vw;
|
||||||
|
margin: -28px calc(50% - 50vw) 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-content {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.category-nav {
|
.category-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|||||||
246
f/web-kboss/src/views/tokenUsage/index.vue
Normal file
246
f/web-kboss/src/views/tokenUsage/index.vue
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<div class="token-usage-page">
|
||||||
|
<div class="usage-shell">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<div class="title-line">
|
||||||
|
<span class="title-icon">
|
||||||
|
<i class="el-icon-data-line"></i>
|
||||||
|
</span>
|
||||||
|
<h2>Token用量</h2>
|
||||||
|
</div>
|
||||||
|
<p>查看模型调用 Token 消耗、调用次数和费用趋势。</p>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" icon="el-icon-refresh" class="refresh-btn">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-row">
|
||||||
|
<div v-for="item in statCards" :key="item.label" class="stat-card" :class="item.type">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<strong>{{ item.value }}</strong>
|
||||||
|
<em>{{ item.desc }}</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<div>
|
||||||
|
<h3>用量明细</h3>
|
||||||
|
<p>按模型维度统计 Token 输入、输出与费用。</p>
|
||||||
|
</div>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
size="small"
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
></el-date-picker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="usageList" class="usage-table" style="width: 100%">
|
||||||
|
<el-table-column prop="modelName" label="模型名称" min-width="180"></el-table-column>
|
||||||
|
<el-table-column prop="calls" label="调用次数" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="inputTokens" label="输入Token" min-width="140"></el-table-column>
|
||||||
|
<el-table-column prop="outputTokens" label="输出Token" min-width="140"></el-table-column>
|
||||||
|
<el-table-column prop="totalTokens" label="总Token" min-width="140"></el-table-column>
|
||||||
|
<el-table-column prop="cost" label="预估费用" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="updatedAt" label="更新时间" min-width="170"></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TokenUsage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dateRange: [],
|
||||||
|
statCards: [
|
||||||
|
{ label: '总消耗 Token', value: '1,286,400', desc: '较昨日 +12.6%', type: 'primary' },
|
||||||
|
{ label: '调用次数', value: '3,482', desc: '今日累计调用', type: 'success' },
|
||||||
|
{ label: '预估费用', value: '¥ 128.64', desc: '按当前单价估算', type: 'warning' },
|
||||||
|
{ label: '活跃模型', value: '8', desc: '最近 7 天有调用', type: 'purple' }
|
||||||
|
],
|
||||||
|
usageList: [
|
||||||
|
{
|
||||||
|
modelName: 'DeepSeek-V4',
|
||||||
|
calls: 1260,
|
||||||
|
inputTokens: '420,000',
|
||||||
|
outputTokens: '318,000',
|
||||||
|
totalTokens: '738,000',
|
||||||
|
cost: '¥ 73.80',
|
||||||
|
updatedAt: '2026-05-25 10:00:00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: '通义千问-Plus',
|
||||||
|
calls: 856,
|
||||||
|
inputTokens: '210,400',
|
||||||
|
outputTokens: '156,000',
|
||||||
|
totalTokens: '366,400',
|
||||||
|
cost: '¥ 38.20',
|
||||||
|
updatedAt: '2026-05-25 09:42:16'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: '腾讯混元-Pro',
|
||||||
|
calls: 342,
|
||||||
|
inputTokens: '102,000',
|
||||||
|
outputTokens: '80,000',
|
||||||
|
totalTokens: '182,000',
|
||||||
|
cost: '¥ 16.64',
|
||||||
|
updatedAt: '2026-05-25 09:18:31'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.token-usage-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 24px;
|
||||||
|
background: linear-gradient(180deg, #f3f7ff 0%, #f7f9fc 48%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-shell {
|
||||||
|
min-height: calc(100vh - 48px);
|
||||||
|
padding: 24px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 12px 30px rgba(31, 45, 61, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header,
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 18px;
|
||||||
|
background: #eef5ff;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header p,
|
||||||
|
.card-header p {
|
||||||
|
margin: 0;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
span,
|
||||||
|
em {
|
||||||
|
display: block;
|
||||||
|
color: #909399;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0 8px;
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: #eef5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.purple {
|
||||||
|
background: #f5f3ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
padding: 22px;
|
||||||
|
border: 1px solid #edf1f7;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-table {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.stat-row {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.stat-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header,
|
||||||
|
.card-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user