From 63a1d4d1f0ee6e658732e67d6cd5cfa1eec77580 Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Fri, 15 May 2026 17:18:51 +0800
Subject: [PATCH 01/15] update
---
b/customer/registerUser.dspy | 77 +++++++++++++++++++++++
b/product/get_firstpage_product_tree.dspy | 2 +-
2 files changed, 78 insertions(+), 1 deletion(-)
diff --git a/b/customer/registerUser.dspy b/b/customer/registerUser.dspy
index 3a97735..4bc33f0 100644
--- a/b/customer/registerUser.dspy
+++ b/b/customer/registerUser.dspy
@@ -1,7 +1,80 @@
+async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, email=None):
+ import aiohttp
+ # 目标URL
+ url = "https://ai.atvoe.com/rbac/usersync"
+ # url = 'https://ai.atvoe.com/tmp/env.dspy'
+
+ # 请求头
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer " + "2i68AZ81di_q5f8AySDrJ"
+ }
+
+ # 请求体数据
+ payload = {
+ "action": "single",
+ "dappid": "cndemo",
+ "user": {
+ "id": userid,
+ "orgid": orgid,
+ "username": username,
+ "name": name,
+ "email": email
+ }
+ }
+
+ try:
+ # 创建一个异步会话
+ result_sysnc = None
+ async with aiohttp.ClientSession() as session:
+ # 发送POST请求
+ async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
+ # 打印响应状态码
+ print(f"状态码: {response.status}")
+ result_sysnc = await response.json()
+
+ if not result_sysnc.get('status') == 'success':
+ print(f"同步用户失败")
+ return {
+ 'status': False
+ }
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ # user_api_keys表格 userid/opc_apikey
+ # 首先判断apikey是否存在
+ apikey = result_sysnc['data'][0].get('apikey')
+ records = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if records:
+ print(f"用户{payload['user']['id']}已存在")
+ return {
+ 'status': False,
+ 'msg': '用户opc_apikey已存在'
+ }
+ await sor.C('user_api_keys', {
+ 'userid': userid,
+ 'opc_apikey': apikey,
+ 'expire_time': None,
+ })
+
+ return {
+ 'status': True,
+ 'msg': '用户同步成功'
+ }
+
+ except Exception as e:
+ print(f"同步用户失败: {e}")
+ return {
+ 'status': False,
+ 'msg': f"同步用户失败: {e}"
+ }
+
+
async def registerUser(ns):
"""
用户注册
"""
+ import re
db = DBPools()
async with db.sqlorContext('kboss') as sor:
if ns:
@@ -176,6 +249,10 @@ async def registerUser(ns):
ns['customerid'] = org_id
await sor.C('customer', ns)
await openCustomerAccounts(sor, org[0]['id'], org_id)
+
+ # 同步用户
+ await sync_cn_ai_user(userid=userid, orgid=ns_org['id'], username=ns['username'], name=ns['username'])
+
return {'status': True, 'msg': '注册成功'}
except Exception as error:
# raise error
diff --git a/b/product/get_firstpage_product_tree.dspy b/b/product/get_firstpage_product_tree.dspy
index 5e07266..63c29b0 100644
--- a/b/product/get_firstpage_product_tree.dspy
+++ b/b/product/get_firstpage_product_tree.dspy
@@ -660,7 +660,7 @@ async def get_firstpage_product_tree(ns={}):
db = DBPools()
async with db.sqlorContext('kboss') as sor:
try:
- if ns.get('url_link') and ('kaiyuancloud' in ns.get('url_link') or 'opencomputing' in ns.get('url_link')):
+ if ns.get('url_link') and ('kaiyuancloud' in ns.get('url_link') or 'opencomputing' in ns.get('url_link') or 'ncmatch' in ns.get('url_link')):
data_baidu = {
"id": "12",
"secTitle": "阿里云",
From 9293263505624978ac34509da1537be70280e136 Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Mon, 18 May 2026 16:20:28 +0800
Subject: [PATCH 02/15] update
---
b/cntoai/get_deerer_header.dspy | 38 +++++++++++++++++++++++++++++++++
b/customer/registerUser.dspy | 26 ++++++++++++++++------
kgadget/src/kgadget.py | 3 +++
3 files changed, 60 insertions(+), 7 deletions(-)
create mode 100644 b/cntoai/get_deerer_header.dspy
diff --git a/b/cntoai/get_deerer_header.dspy b/b/cntoai/get_deerer_header.dspy
new file mode 100644
index 0000000..7bda013
--- /dev/null
+++ b/b/cntoai/get_deerer_header.dspy
@@ -0,0 +1,38 @@
+async def get_deerer_header(ns={}):
+ from appPublic.aes import aes_decode_b64, aes_encode_b64
+ if not ns.get('userid'):
+ userid = await get_user()
+ else:
+ userid = ns.get('userid')
+ if not userid:
+ return {
+ 'status': False,
+ 'msg': '请传递用户ID'
+ }
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ records = await sor.R('user_api_keys', {'userid': userid})
+ if not records:
+ return {
+ 'status': False,
+ 'msg': '未找到匹配的用户'
+ }
+ apikey = records[0]['opc_apikey']
+ appid = records[0]['appid']
+ sk = records[0]['secretkey']
+ if not apikey or not appid or not sk:
+ return {
+ 'status': False,
+ 'msg': '没有找到匹配的用户'
+ }
+ tim = time.time()
+ txt = f'{tim}:{apikey}'
+ cyber = aes_encode_b64(sk, txt)
+ return {
+ 'status': True,
+ 'data': f'Deerer {appid}-:-{cyber}'
+ }
+
+
+ret = await get_deerer_header(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/customer/registerUser.dspy b/b/customer/registerUser.dspy
index 4bc33f0..10efd48 100644
--- a/b/customer/registerUser.dspy
+++ b/b/customer/registerUser.dspy
@@ -1,5 +1,11 @@
async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, email=None):
import aiohttp
+
+ debug(f"sync_cn_ai_user同步用户: {userid}, {orgid}, {username}, {name}, {email}")
+ already_sync_user_key = '2i68AZ81di_q5f8AySDrJ'
+ already_sync_user_dappid = 'cndemo'
+
+
# 目标URL
url = "https://ai.atvoe.com/rbac/usersync"
# url = 'https://ai.atvoe.com/tmp/env.dspy'
@@ -7,13 +13,13 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
# 请求头
headers = {
"Content-Type": "application/json",
- "Authorization": "Bearer " + "2i68AZ81di_q5f8AySDrJ"
+ "Authorization": "Bearer %s" % already_sync_user_key
}
# 请求体数据
payload = {
"action": "single",
- "dappid": "cndemo",
+ "dappid": already_sync_user_dappid,
"user": {
"id": userid,
"orgid": orgid,
@@ -30,11 +36,11 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
# 发送POST请求
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
# 打印响应状态码
- print(f"状态码: {response.status}")
+ debug(f"sync_cn_ai_user状态码: {response.status}")
result_sysnc = await response.json()
if not result_sysnc.get('status') == 'success':
- print(f"同步用户失败")
+ debug(f"sync_cn_ai_user同步用户失败: {result_sysnc}")
return {
'status': False
}
@@ -44,9 +50,12 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
# user_api_keys表格 userid/opc_apikey
# 首先判断apikey是否存在
apikey = result_sysnc['data'][0].get('apikey')
+ appid = result_sysnc['data'][0].get('appid')
+ secretkey = result_sysnc['data'][0].get('secretkey')
+
records = await sor.R('user_api_keys', {'opc_apikey': apikey})
if records:
- print(f"用户{payload['user']['id']}已存在")
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}已存在")
return {
'status': False,
'msg': '用户opc_apikey已存在'
@@ -54,19 +63,22 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
await sor.C('user_api_keys', {
'userid': userid,
'opc_apikey': apikey,
+ 'appid': appid,
+ 'secretkey': secretkey,
'expire_time': None,
})
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
return {
'status': True,
'msg': '用户同步成功'
}
except Exception as e:
- print(f"同步用户失败: {e}")
+ debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
return {
'status': False,
- 'msg': f"同步用户失败: {e}"
+ 'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
}
diff --git a/kgadget/src/kgadget.py b/kgadget/src/kgadget.py
index f40200a..644314d 100644
--- a/kgadget/src/kgadget.py
+++ b/kgadget/src/kgadget.py
@@ -40,6 +40,7 @@ from appPublic.jsonConfig import getConfig
from appPublic.i18n import getI18N
from appPublic.timeUtils import strdate_add
from appPublic.rc4 import unpassword, password
+from appPublic.aes import aes_decode_b64, aes_encode_b64
from ahserver.filedownload import path_encode
@@ -217,6 +218,8 @@ if __name__ == '__main__':
g.KaiYyEnDecryptUtil = KaiYyEnDecryptUtil
g.async_post = async_post
g.jsonpath = jsonpath
+ g.aes_decode_b64 = aes_decode_b64
+ g.aes_encode_b64 = aes_encode_b64
i18n = getI18N(path=workdir)
info(f'gadget version={__version__}')
From d8968afe83bca750fc1e8faf32fc687a9e64e657 Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Mon, 18 May 2026 18:01:33 +0800
Subject: [PATCH 03/15] update
---
b/cntoai/sync_cn_ai_user.dspy | 102 ++++++++++++++++++++++++++++++++++
1 file changed, 102 insertions(+)
create mode 100644 b/cntoai/sync_cn_ai_user.dspy
diff --git a/b/cntoai/sync_cn_ai_user.dspy b/b/cntoai/sync_cn_ai_user.dspy
new file mode 100644
index 0000000..48383ed
--- /dev/null
+++ b/b/cntoai/sync_cn_ai_user.dspy
@@ -0,0 +1,102 @@
+async def sync_cn_ai_user(ns={}):
+ import aiohttp
+
+ user_info = None
+ if ns.get('userid'):
+ userid = ns.get('userid')
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ user_info = await sor.R('users', {'id': userid})
+ if not user_info:
+ return {
+ 'status': False,
+ 'msg': '未找到匹配的用户'
+ }
+ userid = user_info[0]['id']
+ orgid = user_info[0]['orgid']
+ username = user_info[0]['username']
+ name = user_info[0]['name']
+ email = user_info[0]['email']
+
+ already_sync_user_key = '2i68AZ81di_q5f8AySDrJ'
+ already_sync_user_dappid = 'cndemo'
+
+
+ # 目标URL
+ url = "https://ai.atvoe.com/rbac/usersync"
+ # url = 'https://ai.atvoe.com/tmp/env.dspy'
+
+ # 请求头
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer %s" % already_sync_user_key
+ }
+
+ # 请求体数据
+ payload = {
+ "action": "single",
+ "dappid": already_sync_user_dappid,
+ "user": {
+ "id": userid,
+ "orgid": orgid,
+ "username": username,
+ "name": name,
+ "email": email
+ }
+ }
+
+ try:
+ # 创建一个异步会话
+ result_sysnc = None
+ async with aiohttp.ClientSession() as session:
+ # 发送POST请求
+ async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
+ # 打印响应状态码
+ print(f"状态码: {response.status}")
+ result_sysnc = await response.json()
+
+ if not result_sysnc.get('status') == 'success':
+ print(f"同步用户失败: {result_sysnc}")
+ return {
+ 'status': False
+ }
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ # user_api_keys表格 userid/opc_apikey
+ # 首先判断apikey是否存在
+ apikey = result_sysnc['data'][0].get('apikey')
+ appid = result_sysnc['data'][0].get('appid')
+ secretkey = result_sysnc['data'][0].get('secretkey')
+
+ records = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if records:
+ print(f"用户{payload['user']['id']}已存在")
+ return {
+ 'status': False,
+ 'msg': f'用户opc_apikey已存在, {result_sysnc}'
+ }
+ print(f"{result_sysnc}")
+ await sor.C('user_api_keys', {
+ 'userid': userid,
+ 'opc_apikey': apikey,
+ 'appid': appid,
+ 'secretkey': secretkey,
+ 'expire_time': None,
+ })
+
+ return {
+ 'status': True,
+ 'msg': '用户同步成功'
+ }
+
+ except Exception as e:
+ print(f"同步用户失败: {e}")
+ return {
+ 'status': False,
+ 'msg': f"同步用户失败: {e}"
+ }
+
+
+ret = await sync_cn_ai_user(params_kw)
+return ret
\ No newline at end of file
From 314aaa9d855fe9ba7f0de7fae4a0bcb0601f6a5f Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Tue, 19 May 2026 15:37:41 +0800
Subject: [PATCH 04/15] update
---
b/cntoai/get_deerer_header.dspy | 2 +-
b/cntoai/get_user_balance.dspy | 33 +++
b/cntoai/process_user_billing.dspy | 354 +++++++++++++++++++++++++++++
b/customer/registerUser.dspy | 1 +
4 files changed, 389 insertions(+), 1 deletion(-)
create mode 100644 b/cntoai/get_user_balance.dspy
create mode 100644 b/cntoai/process_user_billing.dspy
diff --git a/b/cntoai/get_deerer_header.dspy b/b/cntoai/get_deerer_header.dspy
index 7bda013..ab7fb6a 100644
--- a/b/cntoai/get_deerer_header.dspy
+++ b/b/cntoai/get_deerer_header.dspy
@@ -11,7 +11,7 @@ async def get_deerer_header(ns={}):
}
db = DBPools()
async with db.sqlorContext('kboss') as sor:
- records = await sor.R('user_api_keys', {'userid': userid})
+ records = await sor.R('user_api_keys', {'userid': userid, 'action': 'sync'})
if not records:
return {
'status': False,
diff --git a/b/cntoai/get_user_balance.dspy b/b/cntoai/get_user_balance.dspy
new file mode 100644
index 0000000..bbec0cb
--- /dev/null
+++ b/b/cntoai/get_user_balance.dspy
@@ -0,0 +1,33 @@
+async def get_user_balance(ns={}):
+ """
+ 根据 userid 查询对应机构的客户余额。
+
+ :param userid: 用户 ID
+ :return: 账户余额(与 getCustomerBalance 返回值一致)
+ """
+ apikey = ns.get('apikey')
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ if not apikey:
+ return {
+ 'status': False,
+ 'msg': 'apikey is required'
+ }
+ userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if not userid_li:
+ return {
+ 'status': False,
+ 'msg': '用户未同步到系统'
+ }
+ userid = userid_li[0]['userid']
+ user = await sor.R('users', {'id': userid})
+ orgid = await sor.R('organization', {'id': user[0]['orgid']})
+ balance = await getCustomerBalance(sor, orgid[0]['id'])
+ return {
+ 'status': True,
+ 'balance': balance
+ }
+
+
+ret = await get_user_balance(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/process_user_billing.dspy b/b/cntoai/process_user_billing.dspy
new file mode 100644
index 0000000..f443765
--- /dev/null
+++ b/b/cntoai/process_user_billing.dspy
@@ -0,0 +1,354 @@
+async def _lookup_product(sor, providername, productname):
+ """
+ 按厂商名 + 产品名解析 product 记录。
+ 依次尝试:provider.name + product.name → providerpid → 仅 product.name
+ """
+ provider_list = await sor.R('provider', {'name': providername, 'del_flg': '0'})
+ if provider_list:
+ product_list = await sor.R(
+ 'product',
+ {'name': productname, 'providerid': provider_list[0]['orgid'], 'del_flg': '0'},
+ )
+ if product_list:
+ return product_list[0]
+
+ product_list = await sor.R('product', {'name': productname, 'providerid': provider_list[0]['orgid'], 'del_flg': '0'})
+ if product_list:
+ return product_list[0]
+
+ return None
+
+
+async def _charge_order(sor, orderid, order_type='NEW'):
+ """
+ 确认支付:校验余额 → order2bill → BillAccounting → 更新订单/账单 → customer_goods。
+ 逻辑来自 get_baidu_orderlist.dspy 的 affirmbz_order。
+ """
+ order_rows = await sor.R('bz_order', {'id': orderid})
+ if not order_rows:
+ return {'status': False, 'msg': '订单不存在'}
+
+ order_row = order_rows[0]
+ product_url = None
+
+ await get_business_date(sor=None)
+
+ count = await getCustomerBalance(sor, order_row['customerid'])
+ if count is None:
+ count = 0
+ if count - float(order_row['amount']) < 0:
+ pricedifference = count - round(order_row['amount'], 2)
+ return {
+ 'status': False,
+ 'msg': '账户余额不足',
+ 'pricedifference': round(pricedifference, 2),
+ }
+
+ await order2bill(orderid, sor)
+ bills = await sor.R('bill', {'orderid': orderid, 'del_flg': '0'})
+ try:
+ for bill in bills:
+ ba = BillAccounting(bill)
+ await ba.accounting(sor)
+ dates = datetime.datetime.now()
+ await sor.U('bz_order', {'id': orderid, 'order_status': '1', 'create_at': dates})
+ await sor.U('bill', {'id': orderid, 'bill_state': '1'})
+
+ # 暂时不处理customer_goods
+ # order_goods = await sor.R('order_goods', {'orderid': orderid})
+ # for item in order_goods:
+ # if order_type == 'REFUND':
+ # resource_find_sql = (
+ # "select id from customer_goods where resourceid = '%s';"
+ # % item['resourceids']
+ # )
+ # resource_find_li = await sor.sqlExe(resource_find_sql, {})
+ # resource_find_id = resource_find_li[0]['id']
+ # await sor.U('customer_goods', {'id': resource_find_id, 'del_flg': '1'})
+ # elif order_type == 'RENEW':
+ # resource_find_sql = (
+ # "select id from customer_goods where FIND_IN_SET('%s', resourceid) and del_flg = '0';"
+ # % item['resourceids']
+ # )
+ # resource_find_li = await sor.sqlExe(resource_find_sql, {})
+ # resource_find_id = resource_find_li[0]['id']
+ # await sor.U(
+ # 'customer_goods',
+ # {
+ # 'id': resource_find_id,
+ # 'start_date': item['resourcestarttime'],
+ # 'expire_date': item['resourceendtime'],
+ # },
+ # )
+ # else:
+ # if item.get('chargemode') == 'postpay' and item.get('orderkey') == 'snapshot':
+ # continue
+
+ # product = await sor.R('product', {'id': item['productid']})
+ # now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ # nss = {
+ # 'id': uuid(),
+ # 'providerrid': product[0]['providerid'],
+ # 'productname': product[0]['name'],
+ # 'productdesc': product[0]['description'],
+ # 'customerid': order_row['customerid'],
+ # 'productid': product[0]['id'],
+ # 'specdataid': item.get('spec_id'),
+ # 'orderid': order_row['id'],
+ # 'start_date': item.get('resourcestarttime') or now_str,
+ # 'expire_date': item.get('resourceendtime'),
+ # 'resourceid': item.get('resourceids') or '',
+ # 'orderkey': item.get('orderkey'),
+ # }
+
+ # if product_url:
+ # nss['product_url'] = product_url
+ # else:
+ # spec = (
+ # json.loads(product[0]['spec_note'])
+ # if isinstance(product[0]['spec_note'], str)
+ # else product[0]['spec_note']
+ # )
+ # spec_list_url = [
+ # spec_item['value']
+ # for spec_item in (spec or [])
+ # if spec_item.get('configName') == 'listUrl'
+ # ]
+ # nss['product_url'] = (
+ # spec_list_url[0]
+ # if spec_list_url
+ # else 'https://console.vcp.baidu.com/bcc/#/bcc/instance/list'
+ # )
+
+ # await sor.C('customer_goods', nss)
+ return {'status': True, 'msg': '支付成功'}
+ except Exception as error:
+ return {'status': False, 'msg': str(error)}
+
+async def calc_price_by_saleprotocol(sor, org, product_id, supply_price, quantity=1):
+ """
+ 查 saleprotocol、product_salemode,按折扣计算应付金额。
+
+ :param sor: sqlor 上下文(kboss)
+ :param org: organization 记录,须含 id、parentid
+ :param product_id: product 表主键
+ :param supply_price: 供应价/目录价(折扣前单价,与百度脚本 catalogPrice / itemFee.price 同义)
+ :param quantity: 数量,默认 1
+ :return: dict
+ 成功: status=True, amount(行总金额), price(折后单价), list_price, discount
+ 失败: status=False, msg
+ """
+ try:
+ supply_price = abs(float(supply_price))
+ quantity = int(quantity)
+ except (TypeError, ValueError):
+ return {'status': False, 'msg': 'supply_price / quantity 必须为有效数字'}
+
+ if supply_price <= 0:
+ return {'status': False, 'msg': 'supply_price 必须大于 0'}
+ if quantity <= 0:
+ return {'status': False, 'msg': 'quantity 必须大于 0'}
+
+ saleprotocol_to_person = await sor.R(
+ 'saleprotocol',
+ {'bid_orgid': org['id'], 'offer_orgid': org['parentid'], 'del_flg': '0'},
+ )
+ saleprotocol_to_all = await sor.R(
+ 'saleprotocol',
+ {
+ 'bid_orgid': '*',
+ 'offer_orgid': org['parentid'],
+ 'del_flg': '0',
+ 'salemode': '0',
+ },
+ )
+
+ product_salemode = None
+ if saleprotocol_to_person:
+ product_salemode = await sor.R(
+ 'product_salemode',
+ {
+ 'protocolid': saleprotocol_to_person[0]['id'],
+ 'productid': product_id,
+ 'del_flg': '0',
+ },
+ )
+ if not product_salemode and saleprotocol_to_all:
+ product_salemode = await sor.R(
+ 'product_salemode',
+ {
+ 'protocolid': saleprotocol_to_all[0]['id'],
+ 'productid': product_id,
+ 'del_flg': '0',
+ },
+ )
+ elif saleprotocol_to_all:
+ product_salemode = await sor.R(
+ 'product_salemode',
+ {
+ 'protocolid': saleprotocol_to_all[0]['id'],
+ 'productid': product_id,
+ 'del_flg': '0',
+ },
+ )
+
+ if not product_salemode:
+ return {'status': False, 'msg': '还未上线这个产品的协议配置'}
+
+ discount = product_salemode[0]['discount']
+ list_price = supply_price
+ price = abs(round(list_price * discount, 2))
+ amount = abs(round(price * quantity, 2))
+
+ return {
+ 'status': True,
+ 'amount': amount,
+ 'price': price,
+ 'list_price': list_price,
+ 'discount': discount,
+ 'protocolid': product_salemode[0]['protocolid'],
+ 'product_salemode_id': product_salemode[0].get('id'),
+ }
+
+async def process_user_billing(ns={}):
+ """
+ 通用记账扣费:创建本地订单 → 校验余额 → 出账记账。
+
+ :param userid: 用户 ID
+ :param providername: 厂商名称(写入 bz_order.source,并用于查 product)
+ :param productname: 产品名称(写入 servicename,并用于查 product)
+ :param amount: 扣费金额;use_saleprotocol=False 时为最终扣费额;
+ use_saleprotocol=True 时为供应价/目录价(折扣前单价),走协议算价
+ :param use_saleprotocol: 是否启用 saleprotocol_pricing 协议折扣算价,默认 False 直接按 amount 扣费
+ :param quantity: 仅 use_saleprotocol=True 时生效,数量默认 1
+ :return: dict,含 status、msg;成功时含 orderid、amount
+ """
+ apikey = ns.get('apikey')
+ providername = ns.get('providername')
+ productname = ns.get('productname')
+ amount = ns.get('amount')
+ use_saleprotocol = ns.get('use_saleprotocol', False)
+ quantity = int(ns.get('quantity', 1))
+
+ userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if not userid_li:
+ return {
+ 'status': False,
+ 'msg': '用户未同步到系统'
+ }
+ userid = userid_li[0]['userid']
+
+ try:
+ amount = round(float(amount), 2)
+ except (TypeError, ValueError):
+ return {'status': False, 'msg': 'amount 必须为有效数字'}
+
+ if amount <= 0:
+ return {'status': False, 'msg': 'amount 必须大于 0'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ provider_list = await sor.R('provider', {'name': providername})
+ if not provider_list:
+ return {'status': False, 'msg': '厂商不存在 %s' % providername}
+
+ user_list = await sor.R('users', {'id': userid})
+ if not user_list:
+ return {'status': False, 'msg': '用户不存在 %s' % userid}
+
+ org_list = await sor.R('organization', {'id': user_list[0]['orgid']})
+ if not org_list:
+ return {'status': False, 'msg': '用户所属机构不存在'}
+
+ customerid = org_list[0]['id']
+ product = await _lookup_product(sor, providername, productname)
+ if not product:
+ return {
+ 'status': False,
+ 'msg': '未找到对应产品,请确认 providername/productname 与库中 provider、product 配置一致',
+ }
+
+ list_price = amount
+ unit_price = amount
+ discount = 1
+ originalprice = amount
+
+ if use_saleprotocol:
+ price_res = await calc_price_by_saleprotocol(
+ sor, org_list[0], product['id'], amount, quantity=quantity,
+ )
+ if not price_res['status']:
+ return price_res
+ debug(price_res)
+ debug('list_price %s' % list_price)
+ amount = price_res['amount']
+ list_price = price_res['list_price']
+ unit_price = price_res['price']
+ discount = price_res['discount']
+ originalprice = list_price * quantity
+
+ balance = await getCustomerBalance(sor, customerid)
+ if balance is None:
+ balance = 0
+ if amount > balance:
+ return {
+ 'status': False,
+ 'msg': '账户余额不足',
+ 'pricedifference': round(balance - amount, 2),
+ }
+
+ order_id = uuid()
+ now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ bz_ns = {
+ 'id': order_id,
+ 'order_status': '0',
+ 'business_op': 'BUY',
+ 'userid': userid,
+ 'customerid': customerid,
+ 'order_date': now_str,
+ 'source': providername,
+ 'amount': amount,
+ 'originalprice': round(originalprice, 2),
+ 'ordertype': 'prepay',
+ 'servicename': productname,
+ }
+ await sor.C('bz_order', bz_ns)
+
+ goods_ns = {
+ 'id': uuid(),
+ 'orderid': order_id,
+ 'productid': product['id'],
+ 'providerid': product['providerid'],
+ 'list_price': list_price,
+ 'discount': discount,
+ 'quantity': quantity if use_saleprotocol else 1,
+ 'price': unit_price,
+ 'amount': amount,
+ 'chargemode': 'prepay',
+ 'servicename': productname,
+ 'resourceids': '',
+ 'resourcestarttime': now_str,
+ 'resourceendtime': None,
+ }
+ await sor.C('order_goods', goods_ns)
+
+ charge_res = await _charge_order(sor, order_id, order_type='NEW')
+ if not charge_res['status']:
+ await sor.rollback()
+ return charge_res
+
+ result = {
+ 'status': True,
+ 'msg': '扣费成功',
+ 'orderid': order_id,
+ 'amount': amount,
+ 'productid': product['id'],
+ }
+ if use_saleprotocol:
+ result['discount'] = discount
+ result['list_price'] = list_price
+ result['price'] = unit_price
+ return result
+
+ret = await process_user_billing(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/customer/registerUser.dspy b/b/customer/registerUser.dspy
index 10efd48..e7a4a25 100644
--- a/b/customer/registerUser.dspy
+++ b/b/customer/registerUser.dspy
@@ -65,6 +65,7 @@ async def sync_cn_ai_user(userid=None, orgid=None, username=None, name=None, ema
'opc_apikey': apikey,
'appid': appid,
'secretkey': secretkey,
+ 'action': 'sync',
'expire_time': None,
})
From 0b7c9088adb501b4eb831bd3d9d09f9ca933bf5f Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Tue, 19 May 2026 18:05:41 +0800
Subject: [PATCH 05/15] update
---
b/cntoai/get_user_balance.dspy | 10 +-
b/cntoai/process_user_billing.dspy | 17 +--
b/cntoai/sync_cn_ai_user.dspy | 17 +--
b/customer/registerUser.dspy | 2 +-
b/product/get_firstpage_product_tree.dspy | 166 +++++++++++++---------
5 files changed, 127 insertions(+), 85 deletions(-)
diff --git a/b/cntoai/get_user_balance.dspy b/b/cntoai/get_user_balance.dspy
index bbec0cb..dfc3d55 100644
--- a/b/cntoai/get_user_balance.dspy
+++ b/b/cntoai/get_user_balance.dspy
@@ -6,6 +6,7 @@ async def get_user_balance(ns={}):
:return: 账户余额(与 getCustomerBalance 返回值一致)
"""
apikey = ns.get('apikey')
+ userid = ns.get('userid')
db = DBPools()
async with db.sqlorContext('kboss') as sor:
if not apikey:
@@ -17,10 +18,15 @@ async def get_user_balance(ns={}):
if not userid_li:
return {
'status': False,
- 'msg': '用户未同步到系统'
+ 'msg': 'apikey无效,请联系管理员'
}
- userid = userid_li[0]['userid']
+ # userid = userid_li[0]['userid']
user = await sor.R('users', {'id': userid})
+ if not user:
+ return {
+ 'status': False,
+ 'msg': '用户不存在'
+ }
orgid = await sor.R('organization', {'id': user[0]['orgid']})
balance = await getCustomerBalance(sor, orgid[0]['id'])
return {
diff --git a/b/cntoai/process_user_billing.dspy b/b/cntoai/process_user_billing.dspy
index f443765..5cb17c1 100644
--- a/b/cntoai/process_user_billing.dspy
+++ b/b/cntoai/process_user_billing.dspy
@@ -224,20 +224,13 @@ async def process_user_billing(ns={}):
:return: dict,含 status、msg;成功时含 orderid、amount
"""
apikey = ns.get('apikey')
+ userid = ns.get('userid')
providername = ns.get('providername')
productname = ns.get('productname')
amount = ns.get('amount')
use_saleprotocol = ns.get('use_saleprotocol', False)
quantity = int(ns.get('quantity', 1))
- userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
- if not userid_li:
- return {
- 'status': False,
- 'msg': '用户未同步到系统'
- }
- userid = userid_li[0]['userid']
-
try:
amount = round(float(amount), 2)
except (TypeError, ValueError):
@@ -252,6 +245,14 @@ async def process_user_billing(ns={}):
if not provider_list:
return {'status': False, 'msg': '厂商不存在 %s' % providername}
+ userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if not userid_li:
+ return {
+ 'status': False,
+ 'msg': 'apikey无效,请联系管理员'
+ }
+ # userid = userid_li[0]['userid']
+
user_list = await sor.R('users', {'id': userid})
if not user_list:
return {'status': False, 'msg': '用户不存在 %s' % userid}
diff --git a/b/cntoai/sync_cn_ai_user.dspy b/b/cntoai/sync_cn_ai_user.dspy
index 48383ed..e199667 100644
--- a/b/cntoai/sync_cn_ai_user.dspy
+++ b/b/cntoai/sync_cn_ai_user.dspy
@@ -18,10 +18,10 @@ async def sync_cn_ai_user(ns={}):
name = user_info[0]['name']
email = user_info[0]['email']
+ debug(f"sync_cn_ai_user同步用户: {userid}, {orgid}, {username}, {name}, {email}")
already_sync_user_key = '2i68AZ81di_q5f8AySDrJ'
already_sync_user_dappid = 'cndemo'
-
# 目标URL
url = "https://ai.atvoe.com/rbac/usersync"
# url = 'https://ai.atvoe.com/tmp/env.dspy'
@@ -52,11 +52,11 @@ async def sync_cn_ai_user(ns={}):
# 发送POST请求
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
# 打印响应状态码
- print(f"状态码: {response.status}")
+ debug(f"sync_cn_ai_user状态码: {response.status}")
result_sysnc = await response.json()
if not result_sysnc.get('status') == 'success':
- print(f"同步用户失败: {result_sysnc}")
+ debug(f"sync_cn_ai_user同步用户失败: {result_sysnc}")
return {
'status': False
}
@@ -71,30 +71,31 @@ async def sync_cn_ai_user(ns={}):
records = await sor.R('user_api_keys', {'opc_apikey': apikey})
if records:
- print(f"用户{payload['user']['id']}已存在")
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}已存在")
return {
'status': False,
- 'msg': f'用户opc_apikey已存在, {result_sysnc}'
+ 'msg': '用户opc_apikey已存在'
}
- print(f"{result_sysnc}")
await sor.C('user_api_keys', {
'userid': userid,
'opc_apikey': apikey,
'appid': appid,
'secretkey': secretkey,
+ 'action': 'sync',
'expire_time': None,
})
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
return {
'status': True,
'msg': '用户同步成功'
}
except Exception as e:
- print(f"同步用户失败: {e}")
+ debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
return {
'status': False,
- 'msg': f"同步用户失败: {e}"
+ 'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
}
diff --git a/b/customer/registerUser.dspy b/b/customer/registerUser.dspy
index e7a4a25..0118a5a 100644
--- a/b/customer/registerUser.dspy
+++ b/b/customer/registerUser.dspy
@@ -113,7 +113,7 @@ async def registerUser(ns):
if ns.get('password'):
# 至少8位,包含大小写字母、特殊字符、数字
- if len(ns.get('password')) < 8 or not re.search(r'[a-zA-Z]', ns.get('password')) or not re.search(r'[0-9]', ns.get('password')) or not re.search(r'[!@#$%^&*()_+{}|:"<>?]', ns.get('password')):
+ if len(ns.get('password')) < 8 or not re.search(r'[a-zA-Z]', ns.get('password')) or not re.search(r'[0-9]', ns.get('password')):
return {'status': False, 'msg': '密码至少8位,包含大小写字母、特殊字符、数字'}
if not ns.get('codeid'):
diff --git a/b/product/get_firstpage_product_tree.dspy b/b/product/get_firstpage_product_tree.dspy
index 63c29b0..2e899fc 100644
--- a/b/product/get_firstpage_product_tree.dspy
+++ b/b/product/get_firstpage_product_tree.dspy
@@ -543,7 +543,7 @@ async def get_firstpage_product_tree(ns={}):
"product_service": [
{
"id": "1",
- "firTitle": "云",
+ "firTitle": "基础云",
"secMenu": [
{
"id": "10",
@@ -576,43 +576,77 @@ async def get_firstpage_product_tree(ns={}):
]
},
{
- 'id': "2", 'firTitle': "算", 'secMenu': [
- {
- 'id': '21', 'secTitle': '智算', 'thrMenu': [
+ 'id': "2", 'firTitle': "TOKEN市集", 'secMenu': [
{
- 'id': '211',
- 'thrTitle': None,
- 'value': [#{'id': '2111', 'name': '容器云'},
- {'id': '2113', 'name': '裸金属'},
- #{'id': '2114', 'name': '裸金属-910B'},
- {'id': '2115', 'name': '一体机-昆仑芯'},
- {'id': '2112', 'name': '一体机-天数智芯'},]
- },
- ],
- },
- ]
- },
- {
- "id": "3",
- "firTitle": "网",
- "secMenu": [
- {
- "id": "31",
- "secTitle": "算力网络",
- "thrMenu": [
- {
- "id": "311",
- "thrTitle": None,
- "value": [{'id': '3111', 'name': '互联网专线'},
- {'id': '3121', 'name': 'SDWAN'},
- {'id': '3131', 'name': 'DCI'},
- {'id': '3141', 'name': 'AI专线'}
- ]
- }
- ]
+ # 'id': '21', 'secTitle': '智算', 'thrMenu': [
+ # {
+ # 'id': '211',
+ # 'thrTitle': None,
+ # 'value': [#{'id': '2111', 'name': '容器云'},
+ # {'id': '2113', 'name': '裸金属'},
+ # #{'id': '2114', 'name': '裸金属-910B'},
+ # {'id': '2115', 'name': '一体机-昆仑芯'},
+ # {'id': '2112', 'name': '一体机-天数智芯'},]
+ # },
+ # ],
},
]
},
+ {
+ 'id': "2", 'firTitle': "元境", 'secMenu': [
+ # {
+ # 'id': '21', 'secTitle': '智算', 'thrMenu': [
+ # {
+ # 'id': '211',
+ # 'thrTitle': None,
+ # 'value': [#{'id': '2111', 'name': '容器云'},
+ # {'id': '2113', 'name': '裸金属'},
+ # #{'id': '2114', 'name': '裸金属-910B'},
+ # {'id': '2115', 'name': '一体机-昆仑芯'},
+ # {'id': '2112', 'name': '一体机-天数智芯'},]
+ # },
+ # ],
+ # },
+ ]
+ },
+ # {
+ # 'id': "2", 'firTitle': "算", 'secMenu': [
+ # {
+ # 'id': '21', 'secTitle': '智算', 'thrMenu': [
+ # {
+ # 'id': '211',
+ # 'thrTitle': None,
+ # 'value': [#{'id': '2111', 'name': '容器云'},
+ # {'id': '2113', 'name': '裸金属'},
+ # #{'id': '2114', 'name': '裸金属-910B'},
+ # {'id': '2115', 'name': '一体机-昆仑芯'},
+ # {'id': '2112', 'name': '一体机-天数智芯'},]
+ # },
+ # ],
+ # },
+ # ]
+ # },
+ # {
+ # "id": "3",
+ # "firTitle": "网",
+ # "secMenu": [
+ # {
+ # "id": "31",
+ # "secTitle": "算力网络",
+ # "thrMenu": [
+ # {
+ # "id": "311",
+ # "thrTitle": None,
+ # "value": [{'id': '3111', 'name': '互联网专线'},
+ # {'id': '3121', 'name': 'SDWAN'},
+ # {'id': '3131', 'name': 'DCI'},
+ # {'id': '3141', 'name': 'AI专线'}
+ # ]
+ # }
+ # ]
+ # },
+ # ]
+ # },
# {
# "id": "4",
# "firTitle": "模型",
@@ -623,38 +657,38 @@ async def get_firstpage_product_tree(ns={}):
# "firTitle": "服务",
# "secMenu": []
# },
- {
- "id": "6",
- "firTitle": "用",
- "secMenu": [
- {
- "id": "61",
- "secTitle": "AI应用",
- "thrMenu": [
- {
- "id": "611",
- "thrTitle": "智慧医疗",
- "value": [
- {
- "id": "6111",
- "name": "灵医智能体"
- }
- ]
- },
- {
- "id": "612",
- "thrTitle": "智慧客服",
- "value": [
- {
- "id": "6112",
- "name": "客悦·智能客服"
- }
- ]
- },
- ]
- },
- ]
- }
+ # {
+ # "id": "6",
+ # "firTitle": "用",
+ # "secMenu": [
+ # {
+ # "id": "61",
+ # "secTitle": "AI应用",
+ # "thrMenu": [
+ # {
+ # "id": "611",
+ # "thrTitle": "智慧医疗",
+ # "value": [
+ # {
+ # "id": "6111",
+ # "name": "灵医智能体"
+ # }
+ # ]
+ # },
+ # {
+ # "id": "612",
+ # "thrTitle": "智慧客服",
+ # "value": [
+ # {
+ # "id": "6112",
+ # "name": "客悦·智能客服"
+ # }
+ # ]
+ # },
+ # ]
+ # },
+ # ]
+ # }
]
}
db = DBPools()
From 5aa0fe4303af991bb0b7f3584dd042723bf20f49 Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Wed, 20 May 2026 19:10:26 +0800
Subject: [PATCH 06/15] update
---
b/cntoai/create_model_apikey.dspy | 91 +++++++++++++++++++++++++++
b/cntoai/get_model_apikey.dspy | 85 +++++++++++++++++++++++++
b/cntoai/model_management_add.dspy | 49 +++++++++++++++
b/cntoai/model_management_detail.dspy | 47 ++++++++++++++
b/cntoai/model_management_list.dspy | 25 ++++++++
b/cntoai/model_management_search.dspy | 80 +++++++++++++++++++++++
b/cntoai/model_management_unlist.dspy | 25 ++++++++
b/cntoai/model_management_update.dspy | 45 +++++++++++++
b/cntoai/process_user_billing.dspy | 40 +++++++++---
9 files changed, 477 insertions(+), 10 deletions(-)
create mode 100644 b/cntoai/create_model_apikey.dspy
create mode 100644 b/cntoai/get_model_apikey.dspy
create mode 100644 b/cntoai/model_management_add.dspy
create mode 100644 b/cntoai/model_management_detail.dspy
create mode 100644 b/cntoai/model_management_list.dspy
create mode 100644 b/cntoai/model_management_search.dspy
create mode 100644 b/cntoai/model_management_unlist.dspy
create mode 100644 b/cntoai/model_management_update.dspy
diff --git a/b/cntoai/create_model_apikey.dspy b/b/cntoai/create_model_apikey.dspy
new file mode 100644
index 0000000..5dd88ed
--- /dev/null
+++ b/b/cntoai/create_model_apikey.dspy
@@ -0,0 +1,91 @@
+async def create_model_apikey(ns={}):
+ import aiohttp
+
+ if not ns.get('userid'):
+ ns['userid'] = await get_user()
+
+ if not ns.get('userid'):
+ return {
+ 'status': False,
+ 'msg': '未找到用户'
+ }
+
+ # 通过userid从user_api_keys表中查询opc_apikey
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ records = await sor.R('user_api_keys', {'userid': ns['userid'], 'action': 'sync'})
+ if not records:
+ return {
+ 'status': False,
+ 'msg': '未找到用户opc_apikey'
+ }
+
+ already_sync_user_key = records[0]['opc_apikey']
+ already_sync_user_appid = records[0]['appid']
+
+ # 目标URL
+ base_url = 'https://ai.atvoe.com'
+ url = f"{base_url}/dapi/apply_apikey.dspy"
+
+ # 请求头
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer %s" % already_sync_user_key
+ }
+
+ # 请求体数据
+ payload = {
+ "appname": "cn_ai_user",
+ "description": "",
+ }
+
+ try:
+ # 创建一个异步会话
+ result_sysnc = None
+ async with aiohttp.ClientSession() as session:
+ # 发送POST请求
+ async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
+ # 打印响应状态码
+ debug(f"create_model_apikey状态码: {response.status}")
+ result_sysnc = await response.json()
+
+ if not result_sysnc.get('status') == 'success':
+ debug(f"create_model_apikey创建模型apikey失败: {result_sysnc}")
+ return {
+ 'status': False,
+ 'msg': f"创建模型apikey失败: {result_sysnc}"
+ }
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ # user_api_keys表格 userid/opc_apikey
+ # 首先判断apikey是否存在
+ apikey = result_sysnc['data'][0].get('apikey')
+ appid = result_sysnc['data'][0].get('appid')
+ secretkey = result_sysnc['data'][0].get('secretkey')
+
+ await sor.C('user_api_keys', {
+ 'userid': userid,
+ 'opc_apikey': apikey,
+ 'appid': appid,
+ 'secretkey': secretkey,
+ 'action': 'user_self_create',
+ 'expire_time': None,
+ })
+
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
+ return {
+ 'status': True,
+ 'msg': '用户同步成功'
+ }
+
+ except Exception as e:
+ debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
+ return {
+ 'status': False,
+ 'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
+ }
+
+
+ret = await create_model_apikey(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/get_model_apikey.dspy b/b/cntoai/get_model_apikey.dspy
new file mode 100644
index 0000000..5fea137
--- /dev/null
+++ b/b/cntoai/get_model_apikey.dspy
@@ -0,0 +1,85 @@
+async def get_model_apikey(ns={}):
+ import aiohttp
+
+ if not ns.get('userid'):
+ ns['userid'] = await get_user()
+
+ if not ns.get('userid'):
+ return {
+ 'status': False,
+ 'msg': '未找到用户'
+ }
+
+ # 通过userid从user_api_keys表中查询opc_apikey
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ records = await sor.R('user_api_keys', {'userid': ns['userid'], 'action': 'sync'})
+ if not records:
+ return {
+ 'status': False,
+ 'msg': '未找到用户opc_apikey'
+ }
+
+ already_sync_user_key = records[0]['opc_apikey']
+ already_sync_user_appid = records[0]['appid']
+
+ # 目标URL
+ base_url = 'https://ai.atvoe.com'
+ url = f"{base_url}/dapi/apply_apikey.dspy"
+
+ # 请求头
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer %s" % already_sync_user_key
+ }
+
+ try:
+ # 创建一个异步会话
+ result_sysnc = None
+ async with aiohttp.ClientSession() as session:
+ # 发送GET请求
+ async with session.get(url, headers=headers) as response:
+ # 打印响应状态码
+ debug(f"get_model_apikey状态码: {response.status}")
+ result_sysnc = await response.json()
+
+ if not result_sysnc.get('status') == 'success':
+ debug(f"get_model_apikey获取模型apikey失败: {result_sysnc}")
+ return {
+ 'status': False,
+ 'msg': f"获取模型apikey失败: {result_sysnc}"
+ }
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ # user_api_keys表格 userid/opc_apikey
+ # 首先判断apikey是否存在
+ apikey = result_sysnc['data'][0].get('apikey')
+ appid = result_sysnc['data'][0].get('appid')
+ secretkey = result_sysnc['data'][0].get('secretkey')
+
+ await sor.C('user_api_keys', {
+ 'userid': userid,
+ 'opc_apikey': apikey,
+ 'appid': appid,
+ 'secretkey': secretkey,
+ 'action': 'user_self_create',
+ 'expire_time': None,
+ })
+
+ debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
+ return {
+ 'status': True,
+ 'msg': '用户同步成功'
+ }
+
+ except Exception as e:
+ debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
+ return {
+ 'status': False,
+ 'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
+ }
+
+
+ret = await get_model_apikey(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_add.dspy b/b/cntoai/model_management_add.dspy
new file mode 100644
index 0000000..3f0a114
--- /dev/null
+++ b/b/cntoai/model_management_add.dspy
@@ -0,0 +1,49 @@
+# 可写入/更新的字段(不含 id、created_at、updated_at)
+_MODEL_FIELDS = (
+ 'llmid', 'provider', 'model_name', 'display_name', 'model_type',
+ 'context_length', 'input_token_price', 'output_token_price',
+ 'cache_hit_input_price', 'billing_method', 'billing_unit',
+ 'capabilities', 'limitations', 'highlights', 'is_active',
+ 'description', 'listing_status',
+)
+
+
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+
+def _build_model_dict(ns, include_listing_status=False):
+ data = {}
+ for field in _MODEL_FIELDS:
+ if field in ns and ns.get(field) is not None and ns.get(field) != '':
+ data[field] = ns.get(field)
+ if include_listing_status and 'listing_status' not in data:
+ data['listing_status'] = ns.get('listing_status', 0)
+ return data
+
+
+async def model_management_add(ns={}):
+ """新增模型,默认待上架 listing_status=0"""
+ if not ns.get('provider') or not ns.get('model_name'):
+ return {'status': False, 'msg': 'provider and model_name are required'}
+
+ ns_dic = _build_model_dict(ns, include_listing_status=True)
+ if 'listing_status' not in ns_dic:
+ ns_dic['listing_status'] = 0
+ if 'is_active' not in ns_dic:
+ ns_dic['is_active'] = 1
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ await sor.C('model_management', ns_dic)
+ return {'status': True, 'msg': 'create model success', 'data': ns_dic}
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'create model failed, %s' % str(e)}
+
+
+ret = await model_management_add(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_detail.dspy b/b/cntoai/model_management_detail.dspy
new file mode 100644
index 0000000..69448ec
--- /dev/null
+++ b/b/cntoai/model_management_detail.dspy
@@ -0,0 +1,47 @@
+# 可写入/更新的字段(不含 id、created_at、updated_at)
+_MODEL_FIELDS = (
+ 'llmid', 'provider', 'model_name', 'display_name', 'model_type',
+ 'context_length', 'input_token_price', 'output_token_price',
+ 'cache_hit_input_price', 'billing_method', 'billing_unit',
+ 'capabilities', 'limitations', 'highlights', 'is_active',
+ 'description', 'listing_status',
+)
+
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+
+def _build_model_dict(ns, include_listing_status=False):
+ data = {}
+ for field in _MODEL_FIELDS:
+ if field in ns and ns.get(field) is not None and ns.get(field) != '':
+ data[field] = ns.get(field)
+ if include_listing_status and 'listing_status' not in data:
+ data['listing_status'] = ns.get('listing_status', 0)
+ return data
+
+async def model_management_detail(ns={}):
+ """根据 id 获取单条模型(编辑页回显)"""
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ find_sql = "SELECT * FROM model_management WHERE id = '%s' LIMIT 1;" % _escape(model_id)
+ result = await sor.sqlExe(find_sql, {})
+ if not result:
+ return {'status': False, 'msg': 'model not found'}
+ return {
+ 'status': True,
+ 'msg': 'get model detail success',
+ 'data': result[0],
+ }
+ except Exception as e:
+ return {'status': False, 'msg': 'get model detail failed, %s' % str(e)}
+
+ret = await model_management_detail(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_list.dspy b/b/cntoai/model_management_list.dspy
new file mode 100644
index 0000000..6789f82
--- /dev/null
+++ b/b/cntoai/model_management_list.dspy
@@ -0,0 +1,25 @@
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+async def model_management_list(ns={}):
+ """上架:listing_status 置为 1"""
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ update_sql = """
+ UPDATE model_management SET listing_status = 1 WHERE id = '%s';
+ """ % _escape(model_id)
+ await sor.sqlExe(update_sql, {})
+ return {'status': True, 'msg': 'model listed success'}
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'model list failed, %s' % str(e)}
+
+ret = await model_management_list(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_search.dspy b/b/cntoai/model_management_search.dspy
new file mode 100644
index 0000000..18ad880
--- /dev/null
+++ b/b/cntoai/model_management_search.dspy
@@ -0,0 +1,80 @@
+# 可写入/更新的字段(不含 id、created_at、updated_at)
+_MODEL_FIELDS = (
+ 'llmid', 'provider', 'model_name', 'display_name', 'model_type',
+ 'context_length', 'input_token_price', 'output_token_price',
+ 'cache_hit_input_price', 'billing_method', 'billing_unit',
+ 'capabilities', 'limitations', 'highlights', 'is_active',
+ 'description', 'listing_status',
+)
+
+
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+
+def _build_model_dict(ns, include_listing_status=False):
+ data = {}
+ for field in _MODEL_FIELDS:
+ if field in ns and ns.get(field) is not None and ns.get(field) != '':
+ data[field] = ns.get(field)
+ if include_listing_status and 'listing_status' not in data:
+ data['listing_status'] = ns.get('listing_status', 0)
+ return data
+
+async def model_management_search(ns={}):
+ """
+ 分页查询模型列表,支持按 model_name / model_type / provider 筛选。
+ 返回模型总数、待上架总数、已上架总数。
+ """
+ import traceback
+
+ page_size = int(ns.get('page_size', 100))
+ current_page = int(ns.get('current_page', 1))
+ offset = (current_page - 1) * page_size
+
+ conditions = ['1=1']
+ if ns.get('model_name'):
+ model_name = ns.get('model_name')
+ conditions.append("model_name LIKE '%s'" % model_name)
+ if ns.get('model_type'):
+ conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
+ if ns.get('provider'):
+ conditions.append("provider = '%s'" % _escape(ns.get('provider')))
+ if ns.get('listing_status') is not None and ns.get('listing_status') != '':
+ conditions.append("listing_status = '%s'" % _escape(ns.get('listing_status')))
+
+ where_clause = ' AND '.join(conditions)
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ stats_sql = """SELECT COUNT(*) AS total_count, SUM(CASE WHEN listing_status = 0 THEN 1 ELSE 0 END) AS pending_count, SUM(CASE WHEN listing_status = 1 THEN 1 ELSE 0 END) AS listed_count FROM model_management;"""
+ stats_li = await sor.sqlExe(stats_sql, {})
+ stats = stats_li[0] if stats_li else {}
+
+ count_sql = """SELECT COUNT(*) AS total_count FROM model_management WHERE %s;""" % where_clause
+ filter_total = (await sor.sqlExe(count_sql, {}))[0]['total_count']
+
+ find_sql = """SELECT * FROM model_management WHERE %s ORDER BY updated_at DESC LIMIT %s OFFSET %s;""" % (where_clause, page_size, offset)
+ model_list = await sor.sqlExe(find_sql, {})
+
+ return {
+ 'status': True,
+ 'msg': 'search model success',
+ 'data': {
+ 'total_count': stats.get('total_count', 0),
+ 'pending_count': int(stats.get('pending_count') or 0),
+ 'listed_count': int(stats.get('listed_count') or 0),
+ 'filter_total': filter_total,
+ 'page_size': page_size,
+ 'current_page': current_page,
+ 'model_list': model_list,
+ },
+ }
+ except Exception as e:
+ return {'status': False, 'msg': 'search model failed, %s' % traceback.format_exc()}
+
+ret = await model_management_search(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_unlist.dspy b/b/cntoai/model_management_unlist.dspy
new file mode 100644
index 0000000..8900dda
--- /dev/null
+++ b/b/cntoai/model_management_unlist.dspy
@@ -0,0 +1,25 @@
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+async def model_management_unlist(ns={}):
+ """下架:listing_status 置为 0(统计归入待上架)"""
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ update_sql = """
+ UPDATE model_management SET listing_status = 0 WHERE id = '%s';
+ """ % _escape(model_id)
+ await sor.sqlExe(update_sql, {})
+ return {'status': True, 'msg': 'model unlisted success'}
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'model unlist failed, %s' % str(e)}
+
+ret = await model_management_unlist(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_update.dspy b/b/cntoai/model_management_update.dspy
new file mode 100644
index 0000000..854de52
--- /dev/null
+++ b/b/cntoai/model_management_update.dspy
@@ -0,0 +1,45 @@
+# 可写入/更新的字段(不含 id、created_at、updated_at)
+_MODEL_FIELDS = (
+ 'llmid', 'provider', 'model_name', 'display_name', 'model_type',
+ 'context_length', 'input_token_price', 'output_token_price',
+ 'cache_hit_input_price', 'billing_method', 'billing_unit',
+ 'capabilities', 'limitations', 'highlights', 'is_active',
+ 'description', 'listing_status',
+)
+
+
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+
+def _build_model_dict(ns, include_listing_status=False):
+ data = {}
+ for field in _MODEL_FIELDS:
+ if field in ns and ns.get(field) is not None and ns.get(field) != '':
+ data[field] = ns.get(field)
+ if include_listing_status and 'listing_status' not in data:
+ data['listing_status'] = ns.get('listing_status', 0)
+ return data
+
+async def model_management_update(ns={}):
+ """编辑模型,id 必传"""
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ ns_dic = _build_model_dict(ns)
+ ns_dic['id'] = model_id
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ await sor.U('model_management', ns_dic)
+ return {'status': True, 'msg': 'update model success'}
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'update model failed, %s' % str(e)}
+
+ret = await model_management_update(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/process_user_billing.dspy b/b/cntoai/process_user_billing.dspy
index 5cb17c1..1036eaa 100644
--- a/b/cntoai/process_user_billing.dspy
+++ b/b/cntoai/process_user_billing.dspy
@@ -228,8 +228,15 @@ async def process_user_billing(ns={}):
providername = ns.get('providername')
productname = ns.get('productname')
amount = ns.get('amount')
- use_saleprotocol = ns.get('use_saleprotocol', False)
+ use_saleprotocol = ns.get('use_saleprotocol', True)
quantity = int(ns.get('quantity', 1))
+
+ llmid = ns.get('llmid')
+ if not llmid:
+ return {
+ 'status': False,
+ 'msg': 'llmid必传'
+ }
try:
amount = round(float(amount), 2)
@@ -241,9 +248,22 @@ async def process_user_billing(ns={}):
db = DBPools()
async with db.sqlorContext('kboss') as sor:
- provider_list = await sor.R('provider', {'name': providername})
- if not provider_list:
- return {'status': False, 'msg': '厂商不存在 %s' % providername}
+ product_li = await sor.R('product', {'providerpid': llmid, 'del_flg': '0'})
+ if not product_li:
+ return {
+ 'status': False,
+ 'msg': '未找到对应产品,请确认'
+ }
+ product = product_li[0]
+ productname = product['name']
+ providerid = product['providerid']
+ providername_list = await sor.R('organization', {'id': providerid})
+ if not providername_list:
+ return {
+ 'status': False,
+ 'msg': '厂商不存在 %s' % providername
+ }
+ providername = providername_list[0]['orgname']
userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
if not userid_li:
@@ -262,12 +282,12 @@ async def process_user_billing(ns={}):
return {'status': False, 'msg': '用户所属机构不存在'}
customerid = org_list[0]['id']
- product = await _lookup_product(sor, providername, productname)
- if not product:
- return {
- 'status': False,
- 'msg': '未找到对应产品,请确认 providername/productname 与库中 provider、product 配置一致',
- }
+ # product = await _lookup_product(sor, providername, productname)
+ # if not product:
+ # return {
+ # 'status': False,
+ # 'msg': '未找到对应产品,请确认 providername/productname 与库中 provider、product 配置一致',
+ # }
list_price = amount
unit_price = amount
From 7afd47655de686d08af0c19fc3b7da6fd3b2a73d Mon Sep 17 00:00:00 2001
From: ping <1017253325@qq.com>
Date: Thu, 21 May 2026 20:55:47 +0800
Subject: [PATCH 07/15] update
---
b/cntoai/create_model_apikey.dspy | 47 +--
b/cntoai/get_model_apikey.dspy | 59 ++--
b/cntoai/get_user_balance.dspy | 9 +-
b/cntoai/model_management_add.dspy | 2 +-
.../model_management_customer_search.dspy | 94 ++++++
b/cntoai/model_management_move_down.dspy | 67 +++++
b/cntoai/model_management_pin_top.dspy | 49 +++
b/cntoai/model_management_search.dspy | 9 +-
b/cntoai/process_user_billing.dspy | 282 ++++++++++--------
b/cntoai/sync_cn_ai_user.dspy | 17 +-
10 files changed, 454 insertions(+), 181 deletions(-)
create mode 100644 b/cntoai/model_management_customer_search.dspy
create mode 100644 b/cntoai/model_management_move_down.dspy
create mode 100644 b/cntoai/model_management_pin_top.dspy
diff --git a/b/cntoai/create_model_apikey.dspy b/b/cntoai/create_model_apikey.dspy
index 5dd88ed..5068403 100644
--- a/b/cntoai/create_model_apikey.dspy
+++ b/b/cntoai/create_model_apikey.dspy
@@ -22,10 +22,22 @@ async def create_model_apikey(ns={}):
already_sync_user_key = records[0]['opc_apikey']
already_sync_user_appid = records[0]['appid']
+
+ # domain 从数据库params表中获取到pname=cntoai_domain的pvalue值
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ domain = await sor.R('params', {'pname': 'cntoai_domain'})
+ if domain:
+ domain = domain[0]['pvalue']
+ else:
+ debug(f"create_model_apikey未找到域名")
+ return {
+ 'status': False,
+ 'msg': '未找到域名'
+ }
# 目标URL
- base_url = 'https://ai.atvoe.com'
- url = f"{base_url}/dapi/apply_apikey.dspy"
+ url = f"{domain}/dapi/apply_apikey.dspy"
# 请求头
headers = {
@@ -35,10 +47,11 @@ async def create_model_apikey(ns={}):
# 请求体数据
payload = {
- "appname": "cn_ai_user",
- "description": "",
+ "appname": ns.get('appname'),
+ "description": ns.get('description'),
}
+ # 正常返回的是 {'status': 'ok', 'data': {'id': 'HlEQmcbCA1dX0qjhffA_K', 'name': 'cn_ai_user', 'description': '', 'secretkey': 'QUZVcXg5V1p1STMybG5Ia4r9NHBpkeRw558aATmohvZ7GYptvg==', 'allowedips': None, 'orgid': 'KHtWKY2LENTU4hYYim1Ks'}}
try:
# 创建一个异步会话
result_sysnc = None
@@ -49,7 +62,7 @@ async def create_model_apikey(ns={}):
debug(f"create_model_apikey状态码: {response.status}")
result_sysnc = await response.json()
- if not result_sysnc.get('status') == 'success':
+ if not result_sysnc.get('status') == 'ok':
debug(f"create_model_apikey创建模型apikey失败: {result_sysnc}")
return {
'status': False,
@@ -60,24 +73,22 @@ async def create_model_apikey(ns={}):
async with db.sqlorContext('kboss') as sor:
# user_api_keys表格 userid/opc_apikey
# 首先判断apikey是否存在
- apikey = result_sysnc['data'][0].get('apikey')
- appid = result_sysnc['data'][0].get('appid')
- secretkey = result_sysnc['data'][0].get('secretkey')
+ remote_table_id = result_sysnc['data'].get('id')
+ name = result_sysnc['data'].get('name')
+ secretkey = result_sysnc['data'].get('secretkey')
await sor.C('user_api_keys', {
- 'userid': userid,
- 'opc_apikey': apikey,
- 'appid': appid,
+ 'userid': ns['userid'],
+ 'remote_table_id': remote_table_id,
+ 'name': name,
+ 'opc_apikey': 1,
'secretkey': secretkey,
'action': 'user_self_create',
- 'expire_time': None,
})
-
- debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
- return {
- 'status': True,
- 'msg': '用户同步成功'
- }
+ return {
+ 'status': True,
+ 'msg': '创建模型apikey成功'
+ }
except Exception as e:
debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
diff --git a/b/cntoai/get_model_apikey.dspy b/b/cntoai/get_model_apikey.dspy
index 5fea137..30ba719 100644
--- a/b/cntoai/get_model_apikey.dspy
+++ b/b/cntoai/get_model_apikey.dspy
@@ -23,9 +23,21 @@ async def get_model_apikey(ns={}):
already_sync_user_key = records[0]['opc_apikey']
already_sync_user_appid = records[0]['appid']
+ # domain 从数据库params表中获取到pname=cntoai_domain的pvalue值
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ domain = await sor.R('params', {'pname': 'cntoai_domain'})
+ if domain:
+ domain = domain[0]['pvalue']
+ else:
+ debug(f"get_model_apikey未找到域名")
+ return {
+ 'status': False,
+ 'msg': '未找到域名'
+ }
+
# 目标URL
- base_url = 'https://ai.atvoe.com'
- url = f"{base_url}/dapi/apply_apikey.dspy"
+ url = f"{domain}/dapi/downapps.dspy"
# 请求头
headers = {
@@ -43,7 +55,7 @@ async def get_model_apikey(ns={}):
debug(f"get_model_apikey状态码: {response.status}")
result_sysnc = await response.json()
- if not result_sysnc.get('status') == 'success':
+ if not result_sysnc.get('status') == 'ok':
debug(f"get_model_apikey获取模型apikey失败: {result_sysnc}")
return {
'status': False,
@@ -54,30 +66,33 @@ async def get_model_apikey(ns={}):
async with db.sqlorContext('kboss') as sor:
# user_api_keys表格 userid/opc_apikey
# 首先判断apikey是否存在
- apikey = result_sysnc['data'][0].get('apikey')
- appid = result_sysnc['data'][0].get('appid')
- secretkey = result_sysnc['data'][0].get('secretkey')
+ apikeys = result_sysnc['data']['apikeys']
+ # 遍历apikeys,如果apikey不存在,则创建, 如果存在则做更新 根据userid和remote_table_id判断
+ for apikey_item in apikeys:
+ remote_table_id = apikey_item.get('id')
+ name = '' if not apikey_item.get('name') else apikey_item.get('name')
+ apikeyid = apikey_item.get('apikeyid')
+ exist_record = await sor.R('user_api_keys', {'userid': ns['userid'], 'remote_table_id': remote_table_id})
+ if exist_record:
+ update_sql = f"UPDATE user_api_keys SET name = '{name}', opc_apikey = '{apikeyid}' WHERE userid = '{ns['userid']}' AND remote_table_id = '{remote_table_id}'"
+ await sor.sqlExe(update_sql, {})
+ else:
+ await sor.C('user_api_keys', {
+ 'userid': ns['userid'],
+ 'remote_table_id': remote_table_id,
+ 'name': name,
+ 'opc_apikey': apikeyid,
+ 'action': 'user_self_create',
+ })
- await sor.C('user_api_keys', {
- 'userid': userid,
- 'opc_apikey': apikey,
- 'appid': appid,
- 'secretkey': secretkey,
- 'action': 'user_self_create',
- 'expire_time': None,
- })
-
- debug(f"sync_cn_ai_user用户{payload['user']['id']}同步成功")
- return {
- 'status': True,
- 'msg': '用户同步成功'
- }
+ result_sysnc['status'] = True
+ return result_sysnc
except Exception as e:
- debug(f"sync_cn_ai_user{userid}同步用户失败: {e}")
+ debug(f"get_model_apikey获取模型apikey失败: {e}")
return {
'status': False,
- 'msg': f"sync_cn_ai_user{userid}同步用户失败: {e}"
+ 'msg': f"get_model_apikey获取模型apikey失败: {e}"
}
diff --git a/b/cntoai/get_user_balance.dspy b/b/cntoai/get_user_balance.dspy
index dfc3d55..16ed070 100644
--- a/b/cntoai/get_user_balance.dspy
+++ b/b/cntoai/get_user_balance.dspy
@@ -5,32 +5,33 @@ async def get_user_balance(ns={}):
:param userid: 用户 ID
:return: 账户余额(与 getCustomerBalance 返回值一致)
"""
+ debug(ns)
apikey = ns.get('apikey')
userid = ns.get('userid')
db = DBPools()
async with db.sqlorContext('kboss') as sor:
if not apikey:
return {
- 'status': False,
+ 'status': 'error',
'msg': 'apikey is required'
}
userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
if not userid_li:
return {
- 'status': False,
+ 'status': 'error',
'msg': 'apikey无效,请联系管理员'
}
# userid = userid_li[0]['userid']
user = await sor.R('users', {'id': userid})
if not user:
return {
- 'status': False,
+ 'status': 'error',
'msg': '用户不存在'
}
orgid = await sor.R('organization', {'id': user[0]['orgid']})
balance = await getCustomerBalance(sor, orgid[0]['id'])
return {
- 'status': True,
+ 'status': 'ok',
'balance': balance
}
diff --git a/b/cntoai/model_management_add.dspy b/b/cntoai/model_management_add.dspy
index 3f0a114..a9c0312 100644
--- a/b/cntoai/model_management_add.dspy
+++ b/b/cntoai/model_management_add.dspy
@@ -4,7 +4,7 @@ _MODEL_FIELDS = (
'context_length', 'input_token_price', 'output_token_price',
'cache_hit_input_price', 'billing_method', 'billing_unit',
'capabilities', 'limitations', 'highlights', 'is_active',
- 'description', 'listing_status',
+ 'description', 'listing_status',
)
diff --git a/b/cntoai/model_management_customer_search.dspy b/b/cntoai/model_management_customer_search.dspy
new file mode 100644
index 0000000..f8e066d
--- /dev/null
+++ b/b/cntoai/model_management_customer_search.dspy
@@ -0,0 +1,94 @@
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+# 客户侧可见字段(不含 listing_status、is_active 等运营字段)
+_CUSTOMER_MODEL_COLUMNS = """
+ id, llmid, provider, model_name, display_name, model_type,
+ context_length, input_token_price, output_token_price,
+ cache_hit_input_price, billing_method, billing_unit,
+ capabilities, limitations, highlights, description, sort_order
+"""
+
+
+def _customer_listed_conditions(ns):
+ """已上架且启用的模型;支持按厂商、模型类别筛选"""
+ conditions = ["listing_status = 1", "is_active = 1"]
+ if ns.get('provider'):
+ conditions.append("provider = '%s'" % _escape(ns.get('provider')))
+ if ns.get('model_type'):
+ conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
+ return ' AND '.join(conditions)
+
+async def model_management_customer_search(ns={}):
+ """
+ 客户查看模型列表:仅已上架且启用的模型。
+
+ 可选参数:
+ provider (str) 厂商,精确匹配筛选
+ model_type (str) 模型类别,精确匹配筛选
+ current_page (int) 页码,默认 1
+ page_size (int) 每页条数,默认 10
+
+ 返回 data:
+ provider_list 当前可见模型中的厂商列表(去重)
+ model_type_list 当前可见模型中的模型类别列表(去重)
+ filter_total 当前筛选条件下的模型数量
+ model_list 模型列表
+ page_size, current_page
+
+ 调用示例见 model_management_customer_search.dspy
+ """
+ page_size = int(ns.get('page_size', 1000))
+ current_page = int(ns.get('current_page', 1))
+ offset = (current_page - 1) * page_size
+ where_clause = _customer_listed_conditions(ns)
+ listed_base = "listing_status = 1 AND is_active = 1"
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ provider_sql = """
+ SELECT DISTINCT provider FROM model_management
+ WHERE %s AND provider IS NOT NULL AND provider != ''
+ ORDER BY provider;
+ """ % listed_base
+ model_type_sql = """
+ SELECT DISTINCT model_type FROM model_management
+ WHERE %s AND model_type IS NOT NULL AND model_type != ''
+ ORDER BY model_type;
+ """ % listed_base
+
+ count_sql = """
+ SELECT COUNT(*) AS total_count FROM model_management WHERE %s;
+ """ % where_clause
+ find_sql = """
+ SELECT %s FROM model_management
+ WHERE %s
+ ORDER BY sort_order ASC
+ LIMIT %s OFFSET %s;
+ """ % (_CUSTOMER_MODEL_COLUMNS, where_clause, page_size, offset)
+
+ provider_rows = await sor.sqlExe(provider_sql, {})
+ model_type_rows = await sor.sqlExe(model_type_sql, {})
+ filter_total = (await sor.sqlExe(count_sql, {}))[0]['total_count']
+ model_list = await sor.sqlExe(find_sql, {})
+
+ return {
+ 'status': True,
+ 'msg': 'customer model search success',
+ 'data': {
+ 'provider_list': [r['provider'] for r in provider_rows],
+ 'model_type_list': [r['model_type'] for r in model_type_rows],
+ 'filter_total': filter_total,
+ 'page_size': page_size,
+ 'current_page': current_page,
+ 'model_list': model_list,
+ },
+ }
+ except Exception as e:
+ return {'status': False, 'msg': 'customer model search failed, %s' % str(e)}
+
+ret = await model_management_customer_search(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_move_down.dspy b/b/cntoai/model_management_move_down.dspy
new file mode 100644
index 0000000..d0b9ac6
--- /dev/null
+++ b/b/cntoai/model_management_move_down.dspy
@@ -0,0 +1,67 @@
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+async def model_management_move_down(ns={}):
+ """
+ 下移:与排序上的下一条记录交换 sort_order(已在最后则提示)
+
+ 必填参数:
+ id (int|str) 模型主键
+ """
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ current_sql = """
+ SELECT id, sort_order FROM model_management WHERE id = '%s' LIMIT 1;
+ """ % _escape(model_id)
+ current = await sor.sqlExe(current_sql, {})
+ if not current:
+ return {'status': False, 'msg': 'model not found'}
+
+ cur = current[0]
+ cur_order = int(cur.get('sort_order') or 0)
+ cur_id = int(cur.get('id'))
+
+ next_sql = """
+ SELECT id, sort_order FROM model_management
+ WHERE (sort_order > %s) OR (sort_order = %s AND id > %s)
+ ORDER BY sort_order ASC, id ASC
+ LIMIT 1;
+ """ % (cur_order, cur_order, cur_id)
+ next_row = await sor.sqlExe(next_sql, {})
+ if not next_row:
+ return {'status': True, 'msg': 'already at bottom', 'data': {'sort_order': cur_order}}
+
+ nxt = next_row[0]
+ nxt_order = int(nxt.get('sort_order') or 0)
+ nxt_id = _escape(nxt.get('id'))
+
+ swap_cur_sql = """
+ UPDATE model_management SET sort_order = %s WHERE id = '%s';
+ """ % (nxt_order, _escape(model_id))
+ swap_nxt_sql = """
+ UPDATE model_management SET sort_order = %s WHERE id = '%s';
+ """ % (cur_order, nxt_id)
+ await sor.sqlExe(swap_cur_sql, {})
+ await sor.sqlExe(swap_nxt_sql, {})
+ return {
+ 'status': True,
+ 'msg': 'move down success',
+ 'data': {
+ 'id': model_id,
+ 'sort_order': nxt_order,
+ 'swapped_with_id': nxt.get('id'),
+ },
+ }
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'move down failed, %s' % str(e)}
+
+ret = await model_management_move_down(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_pin_top.dspy b/b/cntoai/model_management_pin_top.dspy
new file mode 100644
index 0000000..3a47250
--- /dev/null
+++ b/b/cntoai/model_management_pin_top.dspy
@@ -0,0 +1,49 @@
+def _escape(value):
+ if value is None:
+ return None
+ return str(value).replace("'", "''")
+
+async def model_management_pin_top(ns={}):
+ """
+ 置顶:将模型排到全局列表最前(sort_order 设为当前最小值 - 1)
+
+ 必填参数:
+ id (int|str) 模型主键
+ """
+ model_id = ns.get('id')
+ if not model_id:
+ return {'status': False, 'msg': 'id is required'}
+
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ try:
+ current_sql = """
+ SELECT id, sort_order FROM model_management WHERE id = '%s' LIMIT 1;
+ """ % _escape(model_id)
+ current = await sor.sqlExe(current_sql, {})
+ if not current:
+ return {'status': False, 'msg': 'model not found'}
+
+ min_sql = "SELECT MIN(sort_order) AS min_order FROM model_management;"
+ min_order = int((await sor.sqlExe(min_sql, {}))[0].get('min_order') or 0)
+ current_order = int(current[0].get('sort_order') or 0)
+
+ if current_order <= min_order:
+ return {'status': True, 'msg': 'already at top', 'data': {'sort_order': current_order}}
+
+ new_order = min_order - 1
+ update_sql = """
+ UPDATE model_management SET sort_order = %s WHERE id = '%s';
+ """ % (new_order, _escape(model_id))
+ await sor.sqlExe(update_sql, {})
+ return {
+ 'status': True,
+ 'msg': 'pin to top success',
+ 'data': {'id': model_id, 'sort_order': new_order},
+ }
+ except Exception as e:
+ await sor.rollback()
+ return {'status': False, 'msg': 'pin to top failed, %s' % str(e)}
+
+ret = await model_management_pin_top(params_kw)
+return ret
\ No newline at end of file
diff --git a/b/cntoai/model_management_search.dspy b/b/cntoai/model_management_search.dspy
index 18ad880..6893ee2 100644
--- a/b/cntoai/model_management_search.dspy
+++ b/b/cntoai/model_management_search.dspy
@@ -4,7 +4,7 @@ _MODEL_FIELDS = (
'context_length', 'input_token_price', 'output_token_price',
'cache_hit_input_price', 'billing_method', 'billing_unit',
'capabilities', 'limitations', 'highlights', 'is_active',
- 'description', 'listing_status',
+ 'description', 'listing_status', 'sort_order',
)
@@ -30,14 +30,15 @@ async def model_management_search(ns={}):
"""
import traceback
- page_size = int(ns.get('page_size', 100))
+ page_size = int(ns.get('page_size', 1000))
current_page = int(ns.get('current_page', 1))
offset = (current_page - 1) * page_size
conditions = ['1=1']
if ns.get('model_name'):
model_name = ns.get('model_name')
- conditions.append("model_name LIKE '%s'" % model_name)
+ # 模糊查询
+ conditions.append(f"model_name LIKE '%%%%{model_name}%%%%'")
if ns.get('model_type'):
conditions.append("model_type = '%s'" % _escape(ns.get('model_type')))
if ns.get('provider'):
@@ -57,7 +58,7 @@ async def model_management_search(ns={}):
count_sql = """SELECT COUNT(*) AS total_count FROM model_management WHERE %s;""" % where_clause
filter_total = (await sor.sqlExe(count_sql, {}))[0]['total_count']
- find_sql = """SELECT * FROM model_management WHERE %s ORDER BY updated_at DESC LIMIT %s OFFSET %s;""" % (where_clause, page_size, offset)
+ find_sql = """SELECT * FROM model_management WHERE %s ORDER BY sort_order ASC LIMIT %s OFFSET %s;""" % (where_clause, page_size, offset)
model_list = await sor.sqlExe(find_sql, {})
return {
diff --git a/b/cntoai/process_user_billing.dspy b/b/cntoai/process_user_billing.dspy
index 1036eaa..03964fd 100644
--- a/b/cntoai/process_user_billing.dspy
+++ b/b/cntoai/process_user_billing.dspy
@@ -26,7 +26,7 @@ async def _charge_order(sor, orderid, order_type='NEW'):
"""
order_rows = await sor.R('bz_order', {'id': orderid})
if not order_rows:
- return {'status': False, 'msg': '订单不存在'}
+ return {'status': 'error', 'msg': '订单不存在'}
order_row = order_rows[0]
product_url = None
@@ -39,7 +39,7 @@ async def _charge_order(sor, orderid, order_type='NEW'):
if count - float(order_row['amount']) < 0:
pricedifference = count - round(order_row['amount'], 2)
return {
- 'status': False,
+ 'status': 'error',
'msg': '账户余额不足',
'pricedifference': round(pricedifference, 2),
}
@@ -123,7 +123,7 @@ async def _charge_order(sor, orderid, order_type='NEW'):
# await sor.C('customer_goods', nss)
return {'status': True, 'msg': '支付成功'}
except Exception as error:
- return {'status': False, 'msg': str(error)}
+ return {'status': 'error', 'msg': str(error)}
async def calc_price_by_saleprotocol(sor, org, product_id, supply_price, quantity=1):
"""
@@ -136,18 +136,18 @@ async def calc_price_by_saleprotocol(sor, org, product_id, supply_price, quantit
:param quantity: 数量,默认 1
:return: dict
成功: status=True, amount(行总金额), price(折后单价), list_price, discount
- 失败: status=False, msg
+ 失败: status='error', msg
"""
try:
supply_price = abs(float(supply_price))
quantity = int(quantity)
except (TypeError, ValueError):
- return {'status': False, 'msg': 'supply_price / quantity 必须为有效数字'}
+ return {'status': 'error', 'msg': 'supply_price / quantity 必须为有效数字'}
if supply_price <= 0:
- return {'status': False, 'msg': 'supply_price 必须大于 0'}
+ return {'status': 'error', 'msg': 'supply_price 必须大于 0'}
if quantity <= 0:
- return {'status': False, 'msg': 'quantity 必须大于 0'}
+ return {'status': 'error', 'msg': 'quantity 必须大于 0'}
saleprotocol_to_person = await sor.R(
'saleprotocol',
@@ -193,7 +193,7 @@ async def calc_price_by_saleprotocol(sor, org, product_id, supply_price, quantit
)
if not product_salemode:
- return {'status': False, 'msg': '还未上线这个产品的协议配置'}
+ return {'status': 'error', 'msg': '还未上线这个产品的协议配置'}
discount = product_salemode[0]['discount']
list_price = supply_price
@@ -217,12 +217,25 @@ async def process_user_billing(ns={}):
:param userid: 用户 ID
:param providername: 厂商名称(写入 bz_order.source,并用于查 product)
:param productname: 产品名称(写入 servicename,并用于查 product)
- :param amount: 扣费金额;use_saleprotocol=False 时为最终扣费额;
+ :param amount: 扣费金额;use_saleprotocol='error' 时为最终扣费额;
use_saleprotocol=True 时为供应价/目录价(折扣前单价),走协议算价
- :param use_saleprotocol: 是否启用 saleprotocol_pricing 协议折扣算价,默认 False 直接按 amount 扣费
+ :param use_saleprotocol: 是否启用 saleprotocol_pricing 协议折扣算价,默认 'error' 直接按 amount 扣费
:param quantity: 仅 use_saleprotocol=True 时生效,数量默认 1
:return: dict,含 status、msg;成功时含 orderid、amount
"""
+ # 存储输入值到usage表
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ usage_ns = {
+ 'id': uuid(),
+ 'userid': ns.get('userid'),
+ 'apikey': ns.get('apikey'),
+ 'llmid': ns.get('llmid'),
+ 'original_price': ns.get('amount'),
+ 'usage_content': json.dumps(ns.get('usage')) if isinstance(ns.get('usage'), dict) else ns.get('usage')
+ }
+ await sor.C('model_usage', usage_ns)
+
apikey = ns.get('apikey')
userid = ns.get('userid')
providername = ns.get('providername')
@@ -234,142 +247,151 @@ async def process_user_billing(ns={}):
llmid = ns.get('llmid')
if not llmid:
return {
- 'status': False,
+ 'status': 'error',
'msg': 'llmid必传'
}
try:
amount = round(float(amount), 2)
except (TypeError, ValueError):
- return {'status': False, 'msg': 'amount 必须为有效数字'}
+ return {'status': 'error', 'msg': 'amount 必须为有效数字'}
if amount <= 0:
- return {'status': False, 'msg': 'amount 必须大于 0'}
+ return {'status': 'error', 'msg': 'amount 必须大于 0'}
db = DBPools()
async with db.sqlorContext('kboss') as sor:
- product_li = await sor.R('product', {'providerpid': llmid, 'del_flg': '0'})
- if not product_li:
- return {
- 'status': False,
- 'msg': '未找到对应产品,请确认'
+ try:
+ product_li = await sor.R('product', {'providerpid': llmid, 'del_flg': '0'})
+ if not product_li:
+ return {
+ 'status': 'error',
+ 'msg': '未找到对应产品,请确认'
+ }
+ product = product_li[0]
+ productname = product['name']
+ providerid = product['providerid']
+ providername_list = await sor.R('organization', {'id': providerid})
+ if not providername_list:
+ return {
+ 'status': 'error',
+ 'msg': '厂商不存在 %s' % providername
+ }
+ providername = providername_list[0]['orgname']
+
+ userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
+ if not userid_li:
+ return {
+ 'status': 'error',
+ 'msg': 'apikey无效,请联系管理员'
+ }
+ # userid = userid_li[0]['userid']
+
+ user_list = await sor.R('users', {'id': userid})
+ if not user_list:
+ return {'status': 'error', 'msg': '用户不存在 %s' % userid}
+
+ org_list = await sor.R('organization', {'id': user_list[0]['orgid']})
+ if not org_list:
+ return {'status': 'error', 'msg': '用户所属机构不存在'}
+
+ customerid = org_list[0]['id']
+ # product = await _lookup_product(sor, providername, productname)
+ # if not product:
+ # return {
+ # 'status': 'error',
+ # 'msg': '未找到对应产品,请确认 providername/productname 与库中 provider、product 配置一致',
+ # }
+
+ list_price = amount
+ unit_price = amount
+ discount = 1
+ originalprice = amount
+
+ if use_saleprotocol:
+ price_res = await calc_price_by_saleprotocol(
+ sor, org_list[0], product['id'], amount, quantity=quantity,
+ )
+ if not price_res['status']:
+ return price_res
+ debug(price_res)
+ debug('list_price %s' % list_price)
+ amount = price_res['amount']
+ list_price = price_res['list_price']
+ unit_price = price_res['price']
+ discount = price_res['discount']
+ originalprice = list_price * quantity
+
+ balance = await getCustomerBalance(sor, customerid)
+ if balance is None:
+ balance = 0
+ if amount > balance:
+ return {
+ 'status': 'error',
+ 'msg': '账户余额不足',
+ 'pricedifference': round(balance - amount, 2),
+ }
+
+ order_id = uuid()
+ now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ bz_ns = {
+ 'id': order_id,
+ 'order_status': '0',
+ 'business_op': 'BUY',
+ 'userid': userid,
+ 'customerid': customerid,
+ 'order_date': now_str,
+ 'source': providername,
+ 'amount': amount,
+ 'originalprice': round(originalprice, 2),
+ 'ordertype': 'prepay',
+ 'servicename': productname,
}
- product = product_li[0]
- productname = product['name']
- providerid = product['providerid']
- providername_list = await sor.R('organization', {'id': providerid})
- if not providername_list:
- return {
- 'status': False,
- 'msg': '厂商不存在 %s' % providername
+ await sor.C('bz_order', bz_ns)
+
+ goods_ns = {
+ 'id': uuid(),
+ 'orderid': order_id,
+ 'productid': product['id'],
+ 'providerid': product['providerid'],
+ 'list_price': list_price,
+ 'discount': discount,
+ 'quantity': quantity if use_saleprotocol else 1,
+ 'price': unit_price,
+ 'amount': amount,
+ 'chargemode': 'prepay',
+ 'servicename': productname,
+ 'resourceids': '',
+ 'resourcestarttime': now_str,
+ 'resourceendtime': None,
}
- providername = providername_list[0]['orgname']
-
- userid_li = await sor.R('user_api_keys', {'opc_apikey': apikey})
- if not userid_li:
- return {
- 'status': False,
- 'msg': 'apikey无效,请联系管理员'
+ await sor.C('order_goods', goods_ns)
+
+ charge_res = await _charge_order(sor, order_id, order_type='NEW')
+ if not charge_res['status']:
+ await sor.rollback()
+ return charge_res
+
+ await sor.U('model_usage', {'id': usage_ns['id'], 'orderid': order_id, 'bill_status': 1})
+
+ result = {
+ 'status': 'ok',
+ 'msg': '扣费成功',
+ 'orderid': order_id,
+ 'amount': amount,
+ 'productid': product['id'],
}
- # userid = userid_li[0]['userid']
-
- user_list = await sor.R('users', {'id': userid})
- if not user_list:
- return {'status': False, 'msg': '用户不存在 %s' % userid}
-
- org_list = await sor.R('organization', {'id': user_list[0]['orgid']})
- if not org_list:
- return {'status': False, 'msg': '用户所属机构不存在'}
-
- customerid = org_list[0]['id']
- # product = await _lookup_product(sor, providername, productname)
- # if not product:
- # return {
- # 'status': False,
- # 'msg': '未找到对应产品,请确认 providername/productname 与库中 provider、product 配置一致',
- # }
-
- list_price = amount
- unit_price = amount
- discount = 1
- originalprice = amount
-
- if use_saleprotocol:
- price_res = await calc_price_by_saleprotocol(
- sor, org_list[0], product['id'], amount, quantity=quantity,
- )
- if not price_res['status']:
- return price_res
- debug(price_res)
- debug('list_price %s' % list_price)
- amount = price_res['amount']
- list_price = price_res['list_price']
- unit_price = price_res['price']
- discount = price_res['discount']
- originalprice = list_price * quantity
-
- balance = await getCustomerBalance(sor, customerid)
- if balance is None:
- balance = 0
- if amount > balance:
+ if use_saleprotocol:
+ result['discount'] = discount
+ result['list_price'] = list_price
+ result['price'] = unit_price
+ return result
+ except Exception as e:
+ sor.rollback()
return {
- 'status': False,
- 'msg': '账户余额不足',
- 'pricedifference': round(balance - amount, 2),
+ 'status': 'error',
+ 'msg': str(e)
}
-
- order_id = uuid()
- now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- bz_ns = {
- 'id': order_id,
- 'order_status': '0',
- 'business_op': 'BUY',
- 'userid': userid,
- 'customerid': customerid,
- 'order_date': now_str,
- 'source': providername,
- 'amount': amount,
- 'originalprice': round(originalprice, 2),
- 'ordertype': 'prepay',
- 'servicename': productname,
- }
- await sor.C('bz_order', bz_ns)
-
- goods_ns = {
- 'id': uuid(),
- 'orderid': order_id,
- 'productid': product['id'],
- 'providerid': product['providerid'],
- 'list_price': list_price,
- 'discount': discount,
- 'quantity': quantity if use_saleprotocol else 1,
- 'price': unit_price,
- 'amount': amount,
- 'chargemode': 'prepay',
- 'servicename': productname,
- 'resourceids': '',
- 'resourcestarttime': now_str,
- 'resourceendtime': None,
- }
- await sor.C('order_goods', goods_ns)
-
- charge_res = await _charge_order(sor, order_id, order_type='NEW')
- if not charge_res['status']:
- await sor.rollback()
- return charge_res
-
- result = {
- 'status': True,
- 'msg': '扣费成功',
- 'orderid': order_id,
- 'amount': amount,
- 'productid': product['id'],
- }
- if use_saleprotocol:
- result['discount'] = discount
- result['list_price'] = list_price
- result['price'] = unit_price
- return result
ret = await process_user_billing(params_kw)
return ret
\ No newline at end of file
diff --git a/b/cntoai/sync_cn_ai_user.dspy b/b/cntoai/sync_cn_ai_user.dspy
index e199667..96e9181 100644
--- a/b/cntoai/sync_cn_ai_user.dspy
+++ b/b/cntoai/sync_cn_ai_user.dspy
@@ -23,8 +23,21 @@ async def sync_cn_ai_user(ns={}):
already_sync_user_dappid = 'cndemo'
# 目标URL
- url = "https://ai.atvoe.com/rbac/usersync"
- # url = 'https://ai.atvoe.com/tmp/env.dspy'
+ # domain 从数据库params表中获取到pname=cntoai_domain的pvalue值
+ domain = None
+ db = DBPools()
+ async with db.sqlorContext('kboss') as sor:
+ domain = await sor.R('params', {'pname': 'cntoai_domain'})
+ if domain:
+ domain = domain[0]['pvalue']
+ else:
+ debug(f"sync_cn_ai_user未找到域名")
+ return {
+ 'status': False,
+ 'msg': '未找到域名'
+ }
+
+ url = f"{domain}/rbac/usersync"
# 请求头
headers = {
From f6743d34d1b84a6fd64823e6dcf45f1f1cadaf9b Mon Sep 17 00:00:00 2001
From: hrx <18603305412@163.com>
Date: Fri, 22 May 2026 11:07:13 +0800
Subject: [PATCH 08/15] updata
---
f/web-kboss/src/api/gotoYuanJing.js | 9 +
f/web-kboss/src/api/model/model.js | 74 ++
f/web-kboss/src/api/newHome.js | 9 +
.../modelManagement/ListingConfirmDialog.vue | 186 ++++
.../modelManagement/ModelDetailDialog.vue | 257 +++++
.../modelManagement/ModelFilter.vue | 134 +++
.../components/modelManagement/ModelStats.vue | 126 +++
f/web-kboss/src/layout/components/Navbar.vue | 12 +
.../layout/components/Sidebar/SidebarItem.vue | 4 +-
.../src/layout/components/Sidebar/index.vue | 103 +-
f/web-kboss/src/permission.js | 2 +-
f/web-kboss/src/router/index.js | 140 ++-
f/web-kboss/src/store/modules/permission.js | 524 ++++++----
f/web-kboss/src/store/modules/user.js | 24 +-
f/web-kboss/src/styles/sidebar.scss | 6 +-
.../homePage/components/topBox/index.vue | 99 +-
.../views/homePage/ncmatch/mainPage/index.vue | 961 ++++++++++++++----
f/web-kboss/src/views/login/indexNew.vue | 7 +-
.../views/modelManagement/AddModelDialog.vue | 422 ++++++++
.../src/views/modelManagement/ApiDocument.vue | 243 +++++
.../src/views/modelManagement/Experience.vue | 546 ++++++++++
.../src/views/modelManagement/ModelDetail.vue | 349 +++++++
.../views/modelManagement/modelManagement.vue | 580 +++++++++++
.../views/operation/operationReport/index.vue | 197 ++++
.../src/views/product/allProduct/index.vue | 536 +++++++++-
.../src/views/tokenManagement/index.vue | 693 +++++++++++++
26 files changed, 5784 insertions(+), 459 deletions(-)
create mode 100644 f/web-kboss/src/api/gotoYuanJing.js
create mode 100644 f/web-kboss/src/api/model/model.js
create mode 100644 f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue
create mode 100644 f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue
create mode 100644 f/web-kboss/src/components/modelManagement/ModelFilter.vue
create mode 100644 f/web-kboss/src/components/modelManagement/ModelStats.vue
create mode 100644 f/web-kboss/src/views/modelManagement/AddModelDialog.vue
create mode 100644 f/web-kboss/src/views/modelManagement/ApiDocument.vue
create mode 100644 f/web-kboss/src/views/modelManagement/Experience.vue
create mode 100644 f/web-kboss/src/views/modelManagement/ModelDetail.vue
create mode 100644 f/web-kboss/src/views/modelManagement/modelManagement.vue
create mode 100644 f/web-kboss/src/views/operation/operationReport/index.vue
create mode 100644 f/web-kboss/src/views/tokenManagement/index.vue
diff --git a/f/web-kboss/src/api/gotoYuanJing.js b/f/web-kboss/src/api/gotoYuanJing.js
new file mode 100644
index 0000000..ee75f2b
--- /dev/null
+++ b/f/web-kboss/src/api/gotoYuanJing.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+// 跳转远景
+export function gotoYuanJingAPI(data) {
+ return request({
+ url: `cntoai/get_deerer_header.dspy`,
+ method: 'get',
+ params: data
+ })
+}
\ No newline at end of file
diff --git a/f/web-kboss/src/api/model/model.js b/f/web-kboss/src/api/model/model.js
new file mode 100644
index 0000000..3397ed4
--- /dev/null
+++ b/f/web-kboss/src/api/model/model.js
@@ -0,0 +1,74 @@
+import request from "@/utils/request";
+// 获取模型列表
+export const reqModelList = (params = {}) => {
+ return request({
+ url: 'cntoai/model_management_search.dspy',
+ method: 'get',
+ params
+ })
+}
+// 上架
+export const reqModelUp = (id) => {
+ return request({
+ url: '/cntoai/model_management_list.dspy',
+ method: 'get',
+ params: { id }
+ })
+}
+// 下架
+export const reqModelDown = (id) => {
+ return request({
+ url: '/cntoai/model_management_unlist.dspy',
+ method: 'get',
+ params: { id }
+ })
+}
+// 模型详情
+export const reqModelDetail = (id) => {
+ return request({
+ url: '/cntoai/model_management_detail.dspy',
+ method: 'get',
+ params: { id }
+ })
+}
+// 编辑模型
+export const reqModelEdit = (data) => {
+ return request({
+ url: '/cntoai/model_management_update.dspy',
+ method: 'get',
+ params: data
+ })
+}
+// 置顶
+export const reqModelTop = (id) => {
+ return request({
+ url: '/cntoai/model_management_pin_top.dspy',
+ method: 'get',
+ params: { id }
+ })
+}
+// 下移
+export const reqModelBottom = (id) => {
+ return request({
+ url: '/cntoai/model_management_move_down.dspy',
+ method: 'get',
+ params: { id }
+ })
+}
+
+// apikey列表
+export const reqApikeyList = (params = {}) => {
+ return request({
+ url: '/cntoai/get_model_apikey.dspy',
+ method: 'get',
+ params
+ })
+}
+// 创建apikey
+export const reqCreateApikey = (params = {}) => {
+ return request({
+ url: '/cntoai/create_model_apikey.dspy',
+ method: 'get',
+ params
+ })
+}
diff --git a/f/web-kboss/src/api/newHome.js b/f/web-kboss/src/api/newHome.js
index a5070db..7165756 100644
--- a/f/web-kboss/src/api/newHome.js
+++ b/f/web-kboss/src/api/newHome.js
@@ -101,3 +101,12 @@ export const todoCount = () => {
method: 'post',
})
}
+
+
+// 获取token市集
+export const reqTokenMarket = () => {
+ return request({
+ url: '/cntoai/model_management_customer_search.dspy',
+ method: 'get',
+ })
+}
\ No newline at end of file
diff --git a/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue b/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue
new file mode 100644
index 0000000..52bc09a
--- /dev/null
+++ b/f/web-kboss/src/components/modelManagement/ListingConfirmDialog.vue
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
{{ action === 'up' ? '确认上架' : '确认下架' }}
+
确认上架该模型到Token市集?
+
确认下架模型后,该模型将从Token市集中移除,用户将无法继续使用。
+
+ 模型名称:{{ getModelDisplayName(model || {}) }}
+
+
+ 取消
+
+ {{ action === 'up' ? '确认上架' : '确认下架' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue b/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue
new file mode 100644
index 0000000..d568b0f
--- /dev/null
+++ b/f/web-kboss/src/components/modelManagement/ModelDetailDialog.vue
@@ -0,0 +1,257 @@
+
+
+
+ 模型详情
+
+
+
+
+
+
+
{{ getModelDisplayName(model) }}
+
模型ID: {{ model.id || '-' }}
+
+
+ {{ getDetailStatusText(model.listing_status) }}
+
+
+
+
+
+ 模型类型
+ {{ model.model_type || '-' }}
+
+
+ 供应商
+ {{ model.provider || '-' }}
+
+
+ 计费方式
+ {{ model.billing_method || '-' }}
+
+
+
+
+
价格信息
+
+
+ 输入
+ ¥{{ formatPrice(model.input_token_price) }}/千Token
+
+
+ 输出
+ ¥{{ formatPrice(model.output_token_price) }}/千Token
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/components/modelManagement/ModelFilter.vue b/f/web-kboss/src/components/modelManagement/ModelFilter.vue
new file mode 100644
index 0000000..df6ade8
--- /dev/null
+++ b/f/web-kboss/src/components/modelManagement/ModelFilter.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/components/modelManagement/ModelStats.vue b/f/web-kboss/src/components/modelManagement/ModelStats.vue
new file mode 100644
index 0000000..4a8f9d2
--- /dev/null
+++ b/f/web-kboss/src/components/modelManagement/ModelStats.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/layout/components/Navbar.vue b/f/web-kboss/src/layout/components/Navbar.vue
index 6b32943..e8999f1 100644
--- a/f/web-kboss/src/layout/components/Navbar.vue
+++ b/f/web-kboss/src/layout/components/Navbar.vue
@@ -731,11 +731,17 @@ export default {
},
async logout() {
store.commit('tagsView/resetBreadcrumbState');
+ store.commit('permission/RESET_ROUTES');
sessionStorage.removeItem("auths");
sessionStorage.removeItem("routes");
sessionStorage.removeItem("user");
sessionStorage.removeItem("userId");
sessionStorage.removeItem("org_type")
+ sessionStorage.removeItem("userType");
+ sessionStorage.removeItem("orgType");
+ sessionStorage.removeItem("roles");
+ sessionStorage.removeItem("juese");
+ sessionStorage.removeItem("jueseNew");
localStorage.removeItem('userId')
localStorage.removeItem("auths");
localStorage.removeItem("routes");
@@ -752,10 +758,16 @@ export default {
let url = window.location.href;
await this.$router.push(`/login`);
store.commit('tagsView/resetBreadcrumbState');
+ store.commit('permission/RESET_ROUTES');
sessionStorage.removeItem("auths");
sessionStorage.removeItem("routes");
sessionStorage.removeItem("user");
sessionStorage.removeItem("userId");
+ sessionStorage.removeItem("userType");
+ sessionStorage.removeItem("orgType");
+ sessionStorage.removeItem("roles");
+ sessionStorage.removeItem("juese");
+ sessionStorage.removeItem("jueseNew");
},
changeColor() {
this.dialogFormVisible = false
diff --git a/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue b/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue
index da82b10..37615ef 100644
--- a/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue
+++ b/f/web-kboss/src/layout/components/Sidebar/SidebarItem.vue
@@ -97,12 +97,12 @@ export default {
// 给嵌套菜单添加左边距
::v-deep .nest-menu {
.el-menu-item {
- padding-left: 60px !important; // 或者您想要的任何值,比如10px
+ padding-left: 42px !important; // 子菜单稍微缩进,同时保留蓝底圆角选中态
}
// 如果还有更深层的嵌套,可以继续设置
.nest-menu .el-menu-item {
- padding-left: 100px !important;
+ padding-left: 64px !important;
}
}
}
diff --git a/f/web-kboss/src/layout/components/Sidebar/index.vue b/f/web-kboss/src/layout/components/Sidebar/index.vue
index 9763d74..a977547 100644
--- a/f/web-kboss/src/layout/components/Sidebar/index.vue
+++ b/f/web-kboss/src/layout/components/Sidebar/index.vue
@@ -11,7 +11,7 @@
:text-color="variables.menuText"
:unique-opened="true"
:active-text-color="variables.menuActiveText"
- :collapse-transition="false"
+ :collapse-transition="true"
:default-active="activeMenu"
mode="vertical"
class="el-menu-vertical"
@@ -25,6 +25,14 @@
/>
+
@@ -72,6 +80,12 @@ export default {
mounted() {
console.log("Sidebar mounted - 权限路由:", this.permissionRoutes);
+ },
+
+ methods: {
+ toggleSideBar() {
+ this.$store.dispatch("app/toggleSideBar");
+ }
}
};
@@ -88,6 +102,8 @@ export default {
display: flex;
flex-direction: column;
box-sizing: border-box;
+ position: relative;
+ transition: width 0.25s ease;
}
.menu-scroll-container {
@@ -107,6 +123,36 @@ export default {
}
}
+ .sidebar-collapse-btn {
+ position: absolute;
+ right: 16px;
+ bottom: 18px;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 42px;
+ height: 42px;
+ color: #8a94a6;
+ font-size: 18px;
+ cursor: pointer;
+ background: #ffffff;
+ border: 1px solid #eef2f7;
+ border-radius: 50%;
+ box-shadow: 0 8px 22px rgba(31, 45, 61, 0.12);
+ transition: all 0.25s ease;
+
+ &:hover {
+ color: #1e6fff;
+ transform: translateY(-2px);
+ box-shadow: 0 12px 26px rgba(30, 111, 255, 0.18);
+ }
+
+ &.collapsed {
+ right: 11px;
+ }
+ }
+
// 更具体的选择器
::v-deep .el-menu-vertical {
border: none;
@@ -117,9 +163,37 @@ export default {
.el-submenu__title,
.el-menu-item {
+ height: 56px;
+ line-height: 56px;
+ margin: 6px 14px;
+ padding: 0 18px !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ border-radius: 12px;
+ transition: all 0.25s ease;
+ }
+
+ .el-submenu__title span,
+ .el-menu-item span {
+ transition: opacity 0.2s ease, transform 0.2s ease;
+ }
+
+ .el-submenu__title:hover,
+ .el-menu-item:not(.is-active):hover {
+ color: #1e6fff !important;
+ background: #f4f8ff !important;
+ }
+
+ .el-menu-item.is-active {
+ color: #ffffff !important;
+ background: linear-gradient(135deg, #1e6fff, #5d8dff) !important;
+
+ }
+
+ .el-menu-item.is-active i,
+ .el-menu-item.is-active span {
+ color: #ffffff !important;
}
// 子菜单容器
@@ -128,23 +202,18 @@ export default {
.el-menu-item {
// 激活的子菜单项
&.is-active {
- background-color: #d7dafd !important;
- color: #296ad9 !important;
+ background: linear-gradient(135deg, #1e6fff, #5d8dff) !important;
+ color: #ffffff !important;
+
&:before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background-color: #296ad9;
+ display: none;
}
}
// 非激活状态的悬停效果
&:not(.is-active):hover {
- background-color: #f5f7fa !important;
+ background-color: #f4f8ff !important;
}
}
}
@@ -157,16 +226,24 @@ export default {
.el-submenu__title,
.el-menu-item {
+ margin: 6px 8px;
+ padding: 0 !important;
text-overflow: clip;
justify-content: center;
}
+ .el-submenu__title span,
+ .el-menu-item span {
+ opacity: 0;
+ transform: translateX(-6px);
+ }
+
// 折叠状态下的子菜单激活样式
.el-menu--popup {
.el-menu-item {
&.is-active {
- background-color: #f5f7fa !important;
- color: #296ad9 !important;
+ color: #ffffff !important;
+ background: linear-gradient(135deg, #1e6fff 0%, #244fbd 100%) !important;
}
}
}
diff --git a/f/web-kboss/src/permission.js b/f/web-kboss/src/permission.js
index 891d831..7420a79 100644
--- a/f/web-kboss/src/permission.js
+++ b/f/web-kboss/src/permission.js
@@ -11,7 +11,7 @@ import {getHomePath} from "@/views/setting/tools";
NProgress.configure({showSpinner: false}); // NProgress Configuration
-const whiteList = ["/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist
+const whiteList = ["product","/login", "/homePage", "/registrationPage", "/shoppingCart", "/homePageImage","/h5HomePage",'/H5about','/modelProductDetail','/ncmatchHome']; // no redirect whitelist
// 获取用户代理字符串
const userAgent = window.navigator.userAgent;
diff --git a/f/web-kboss/src/router/index.js b/f/web-kboss/src/router/index.js
index cc39ae4..55b152d 100644
--- a/f/web-kboss/src/router/index.js
+++ b/f/web-kboss/src/router/index.js
@@ -404,11 +404,36 @@ export const constantRoutes = [
* 需要根据用户角色动态加载的路由
*/
export const asyncRoutes = [
+ // 运营——模型管理
+ {
+ path: "/modelManagement",
+ component: Layout,
+ meta: {
+ // title 是菜单上显示的文字,fullPath 用来和后端权限 path 对权限。
+ title: "模型管理",
+ fullPath: "/modelManagement",
+ noCache: true,
+ // icon 是左侧菜单图标,roles 限制只有运营角色能看到。
+ icon: "el-icon-cpu",
+ roles: ["运营"]
+ },
+ children: [
+ {
+ path: "",
+ component: () => import('@/views/modelManagement/modelManagement.vue'),
+ name: 'modelManagement',
+ meta: {
+ title: "模型管理",
+ fullPath: "/modelManagement",
+ noCache: true,
+ roles: ["运营"]
+ }
+ },
+ ]
+ },
-
- // 全部产品 - 一级菜单
- // 全部产品 - 一级菜单(无子路由)
+ // token市集 - 一级菜单(所有登录用户都能看到)
{
path: "/product",
component: Layout,
@@ -416,7 +441,7 @@ export const asyncRoutes = [
title: "全部产品",
fullPath: "/product",
noCache: true,
- icon: "el-icon-goods"
+ icon: "el-icon-coin"
},
children: [
{
@@ -431,6 +456,66 @@ export const asyncRoutes = [
},
]
},
+ // 令牌管理 - 一级菜单(所有登录用户都能看到)
+ {
+ path: "/tokenManagement",
+ component: Layout,
+ meta: {
+ title: "令牌管理",
+ fullPath: "/tokenManagement",
+ noCache: true,
+ icon: "el-icon-key"
+ },
+ children: [
+ {
+ path: "",
+ component: () => import('@/views/tokenManagement/index.vue'),
+ name: 'TokenManagement',
+ meta: {
+ title: "令牌管理",
+ fullPath: "/tokenManagement",
+ noCache: true,
+ icon: "el-icon-key"
+ }
+ },
+ ]
+ },
+ // 模型体验
+ {
+ path: "/modelExperience",
+ component: () => import('@/views/modelManagement/Experience.vue'),
+ hidden: true,
+ name: 'modelExperience',
+ meta: {
+ title: "模型体验",
+ fullPath: "/modelExperience",
+ 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",
component: Layout,
@@ -453,6 +538,32 @@ export const asyncRoutes = [
}
]
},
+ // 运营——运营报表
+ {
+ path: "/operationReport",
+ component: Layout,
+ meta: {
+ title: "运营报表",
+ fullPath: "/operationReport",
+ noCache: true,
+ icon: "el-icon-data-analysis",
+ roles: ["运营"]
+ },
+ children: [
+ {
+ path: "",
+ component: () => import('@/views/operation/operationReport/index.vue'),
+ name: 'operationReport',
+ meta: {
+ title: "运营报表",
+ fullPath: "/operationReport",
+ noCache: true,
+ icon: "el-icon-data-analysis",
+ roles: ["运营"]
+ }
+ }
+ ]
+ },
{
path: "/orderManagement",
@@ -565,13 +676,15 @@ export const asyncRoutes = [
path: "/consultingMangement",
name: 'ConsultingMangement',
component: Layout,
- meta: { title: "咨询表单", fullPath: "/consultingMangement", noCache: true, icon: "el-icon-s-platform" },
+ // 咨询表单是表单/订单类入口,所以菜单图标用 el-icon-s-order。
+ meta: { title: "咨询表单", fullPath: "/consultingMangement", noCache: true, icon: "el-icon-s-order" },
children: [
{
path: "index",
component: () => import('@/views/operation/consultingMangement/index.vue'),
name: 'ConsultingMangement',
- meta: { title: "咨询表单", fullPath: "/consultingMangement/index", noCache: true, icon: "el-icon-s-platform" },
+ // 子路由也带 icon,单子菜单折叠成一级菜单时能继续显示图标。
+ meta: { title: "咨询表单", fullPath: "/consultingMangement/index", noCache: true, icon: "el-icon-s-order" },
}
]
},
@@ -1180,7 +1293,8 @@ export const asyncRoutes = [
component: Layout,
name: "qualificationReview",
redirect: "/qualificationReview/index",
- meta: { fullPath: "/qualificationReview", title: "资质审核", noCache: true, icon: 'el-icon-s-home' },
+ // 资质审核是审核/校验类菜单,所以用 el-icon-s-check。
+ meta: { fullPath: "/qualificationReview", title: "资质审核", noCache: true, icon: 'el-icon-s-check' },
children: [
{
path: "noApproveInfo",
@@ -1203,7 +1317,8 @@ export const asyncRoutes = [
component: Layout,
name: "approveMangement",
redirect: "/approveMangement/index",
- meta: { fullPath: "/approveMangement", title: "供需审核", noCache: true, icon: 'el-icon-s-home' },
+ // 供需审核表示供给和需求两边协作审核,所以用 el-icon-s-cooperation。
+ meta: { fullPath: "/approveMangement", title: "供需审核", noCache: true, icon: 'el-icon-s-cooperation' },
children: [
{
path: "pendingPro",
@@ -1263,12 +1378,14 @@ export const asyncRoutes = [
},
]
},
+
{
path: "/menuMangement",
component: Layout,
name: "menuMangement",
redirect: "/menuMangement/index",
- meta: { fullPath: "/menuMangement", title: "菜单管理", noCache: true, icon: 'el-icon-s-home' },
+ // 菜单管理就是维护菜单配置,用 Element UI 的菜单图标。
+ meta: { fullPath: "/menuMangement", title: "菜单管理", noCache: true, icon: 'el-icon-menu' },
children: [
{
path: "index",
@@ -1435,7 +1552,8 @@ export const asyncRoutes = [
{
path: "/operation", component: Layout, redirect: "/operation/supplierManagement", meta: {
- title: "运营", icon: "el-icon-s-tools", noCache: true, fullPath: "/operation",
+ // 运营是运营后台入口,用操作/运营类图标。
+ title: "运营", icon: "el-icon-s-operation", noCache: true, fullPath: "/operation",
}, children: [
{
@@ -1559,7 +1677,7 @@ export const asyncRoutes = [
hidden: true,
path: "colony",
- title: "管理集群",
+ title: "管理集群",
component: () => import("@/views/operation/computingCenterManagement/colony/index.vue"),
name: "supplierManagement",
meta: {
diff --git a/f/web-kboss/src/store/modules/permission.js b/f/web-kboss/src/store/modules/permission.js
index d302399..b59a9f4 100644
--- a/f/web-kboss/src/store/modules/permission.js
+++ b/f/web-kboss/src/store/modules/permission.js
@@ -1,24 +1,197 @@
-// permission.js - 修改后的完整代码
import { asyncRoutes, constantRoutes } from "@/router";
-// 获取用户代理字符串
-const userAgent = window.navigator.userAgent;
+// 用浏览器 UA 判断当前是不是手机端,后面会按 PC / 手机过滤菜单。
+const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
-// 判断是否为移动设备
-const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
+// 项目里用到的固定角色名,集中放这里,避免代码里到处写字符串。
+const CUSTOMER_ROLE = '客户';
+const OPERATION_ROLE = '运营';
-// 如果是移动设备,添加移动端首页路由和根路径重定向
+// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。
+const SPECIAL_ORDER_USER = 'ZhipuHZ';
+
+// 超级管理员只放行这个一级菜单。
+const SUPER_ADMIN_ROUTE_PATH = '/superAdministrator';
+
+// 所有登录用户都能访问的公共路由,不依赖后端 auths 和角色。hidden 路由不会显示在菜单里。
+const COMMON_ROUTE_PATHS = ['/product', '/tokenManagement', '/modelExperience', '/modelDetail', '/modelApiDocument'];
+
+// 运营角色需要额外补出来的菜单。
+const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/operationReport'];
+
+// 普通客户账号默认要补出来的基础菜单。
+const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement'];
+
+// 客户角色额外能看到的一级菜单。
+const CUSTOMER_EXTRA_ROUTE_PATHS = [
+ '/unsubscribeManagement',
+ '/informationPerfect',
+ '/rechargeManagement',
+ '/invoiceManagement',
+ '/workOrderManagement'
+];
+
+// 这些菜单只允许客户角色看到,非客户就算后端给了权限也不展示。
+const CUSTOMER_ONLY_ROUTE_PATHS = [
+ '/overview',
+ ...CUSTOMER_EXTRA_ROUTE_PATHS
+];
+
+// 客户登录后必须能看到的入口菜单,不完全依赖后端 auths 返回。
+const CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS = ['/overview'];
+
+// 订单管理里只给 SPECIAL_ORDER_USER 看的子菜单 path。
+const ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER = ['HistoricalOrders', 'orderDetails'];
+
+const isMobile = MOBILE_UA_REGEXP.test(window.navigator.userAgent);
+
+// 把角色统一整理成数组,兼容 undefined、数组、逗号字符串这几种写法。
+function normalizeRoles(roles) {
+ if (!roles) {
+ return [];
+ }
+
+ if (Array.isArray(roles)) {
+ return roles;
+ }
+
+ if (typeof roles === 'string') {
+ return roles.split(',').filter(Boolean);
+ }
+
+ return [];
+}
+
+// 从 sessionStorage 里取 roles,取不到或格式坏了就当成没有角色。
+function getSessionRoles() {
+ try {
+ return JSON.parse(sessionStorage.getItem('roles') || '[]');
+ } catch (error) {
+ console.warn('读取 roles 失败:', error);
+ return [];
+ }
+}
+
+// 汇总当前用户的所有角色来源:接口参数、vuex、sessionStorage、新旧字段。
+function getCurrentRoles(params, rootState) {
+ return [
+ ...normalizeRoles(params.roles),
+ ...normalizeRoles(rootState.user.roles),
+ ...normalizeRoles(getSessionRoles()),
+ ...normalizeRoles(sessionStorage.getItem('jueseNew'))
+ ];
+}
+
+// 判断当前用户是不是客户角色。
+function isCustomer(userRoles = []) {
+ return userRoles.includes(CUSTOMER_ROLE);
+}
+
+// 把布尔值转成更好读的设备类型,后面的判断都用 pc / mobile。
+function getDeviceType(isMobileDevice) {
+ return isMobileDevice ? 'mobile' : 'pc';
+}
+
+// 判断路由 meta.roles 是否满足。没写 roles 的路由默认所有角色都能继续往下判断。
+function hasRouteRole(route, userRoles = []) {
+ const routeRoles = route.meta?.roles;
+
+ if (!routeRoles || routeRoles.length === 0) {
+ return true;
+ }
+
+ return routeRoles.some(role => userRoles.includes(role));
+}
+
+// 根据设备过滤路由:手机只要手机路由,PC 不要手机专用路由。
+function isRouteAllowedByDevice(route, deviceType) {
+ if (deviceType === 'mobile') {
+ return route.meta?.isMobile || route.meta?.isMobile === true;
+ }
+
+ return route.meta?.isMobile !== true;
+}
+
+// 在一组路由里按 path 找某个路由。
+function findRouteByPath(routes, path) {
+ return routes.find(route => route.path === path);
+}
+
+// 后端 auths 里的 path 要和路由 meta.fullPath 对上,对上才算有权限。
+function routeHasPermission(route, permissions) {
+ return permissions.some(permission => permission.path === route.meta?.fullPath);
+}
+
+// 客户专属菜单要再卡一层客户角色,防止非客户误展示。
+function canShowCustomerOnlyRoute(route, userRoles) {
+ return !CUSTOMER_ONLY_ROUTE_PATHS.includes(route.path) || isCustomer(userRoles);
+}
+
+// 把所有动态路由的 fullPath 收集出来。后端返回 path 为空时,表示拥有全部权限。
+function getAllRoutePermissions(routes) {
+ const permissions = [];
+
+ routes.forEach(route => {
+ if (route.meta?.fullPath) {
+ permissions.push({ path: route.meta.fullPath });
+ }
+
+ if (route.children) {
+ permissions.push(...getAllRoutePermissions(route.children));
+ }
+ });
+
+ return permissions;
+}
+
+// 复制路由对象。这里不能用 JSON 深拷贝,因为路由里的 component 是函数,会被 JSON 丢掉。
+function cloneRoute(route) {
+ const clonedRoute = { ...route };
+
+ if (route.meta) {
+ clonedRoute.meta = { ...route.meta };
+ }
+
+ if (route.children) {
+ clonedRoute.children = route.children.map(cloneRoute);
+ }
+
+ return clonedRoute;
+}
+
+// 根据 path 列表批量找到对应路由,没找到的自动过滤掉。
+function getRoutesByPath(routes, paths) {
+ return paths
+ .map(path => findRouteByPath(routes, path))
+ .filter(Boolean);
+}
+
+// 判断这个一级路由是不是已经加过了,避免菜单重复出现。
+function shouldAppendRoute(accessedRoutes, route) {
+ return !accessedRoutes.some(item => item.path === route.path);
+}
+
+// 把缺少的路由补到最终菜单里,补之前会先去重并复制一份。
+function appendMissingRoutes(accessedRoutes, routesToAppend) {
+ routesToAppend.forEach(route => {
+ if (shouldAppendRoute(accessedRoutes, route)) {
+ accessedRoutes.push(cloneRoute(route));
+ }
+ });
+
+ return accessedRoutes;
+}
+
+// 如果是手机访问,额外把根路径导到 H5 首页,并注册 H5 首页菜单。
if (isMobile) {
console.log("检测到移动设备,添加移动端路由");
- // 先添加根路径重定向到移动端首页
constantRoutes.unshift({
path: '/',
redirect: '/h5HomePage',
hidden: true
});
- // 添加移动端首页路由
constantRoutes.push({
path: '/h5HomePage',
name: 'H5HomePage',
@@ -82,106 +255,165 @@ if (isMobile) {
});
}
-// 修复:更全面的路由过滤逻辑
+// 核心过滤函数:拿后端权限、角色和设备类型,一层层筛出最终可访问路由。
function filterAsyncRoutes(routes, permissions, userRoles = [], deviceType = 'pc') {
const res = [];
- // 定义需要客户角色才能访问的路由
- const customerOnlyRoutes = [
- "/product", "/overview", "/workOrderManagement",
- "/unsubscribeManagement", "/informationPerfect",
- "/rechargeManagement", "/invoiceManagement"
- ];
-
routes.forEach(route => {
- // 创建路由副本
- const tmpRoute = { ...route };
+ // 先复制一份,避免直接改原始 asyncRoutes。
+ const tmpRoute = cloneRoute(route);
- // 检查当前路由是否在权限列表中
- const hasPermission = permissions.some(p => p.path === route.meta?.fullPath);
-
- // 特殊处理:确保"全部产品"和"资源概览"这两个一级路由在客户角色下显示
- const isCriticalRoute = route.path === "/product" || route.path === "/overview";
-
- // 检查是否为仅客户可访问的路由
- const isCustomerOnlyRoute = customerOnlyRoutes.includes(route.path);
-
- // 如果路由需要客户角色,但用户不是客户,则跳过
- if (isCustomerOnlyRoute && !userRoles.includes('客户')) {
- return; // 跳过当前路由
+ // 第一步:角色不符合,或者客户专属菜单但当前用户不是客户,直接跳过。
+ if (!hasRouteRole(tmpRoute, userRoles) || !canShowCustomerOnlyRoute(route, userRoles)) {
+ return;
}
- // 新增:根据设备类型过滤路由
- if (deviceType === 'mobile' && !(route.meta?.isMobile || route.meta?.isMobile === true)) {
- return; // 移动设备跳过非移动端路由
- }
- if (deviceType === 'pc' && route.meta?.isMobile === true) {
- return; // PC设备跳过移动端路由
+ // 第二步:设备不符合也跳过,比如 PC 端不展示 H5 专用路由。
+ if (!isRouteAllowedByDevice(route, deviceType)) {
+ return;
}
- // 如果当前路由有权限,则加入结果
- if (hasPermission) {
+ // 第三步:看后端 auths 里有没有当前路由的 fullPath。
+ const hasPermission = routeHasPermission(route, permissions);
+
+ // 第四步:客户首页入口特殊处理,客户登录后默认展示。
+ const isAlwaysVisibleCustomerRoute =
+ CUSTOMER_ALWAYS_VISIBLE_ROUTE_PATHS.includes(route.path) && isCustomer(userRoles);
+
+ // 有权限,或者是客户默认入口,就把这个路由放进最终菜单。
+ if (hasPermission || isAlwaysVisibleCustomerRoute) {
res.push(tmpRoute);
- }
- // 如果是关键路由且用户是客户,也要加入结果
- else if (isCriticalRoute && userRoles.includes('客户')) {
- res.push(tmpRoute);
- }
- // 如果没有直接权限,但有子路由,递归处理子路由
- else if (tmpRoute.children) {
+ } else if (tmpRoute.children) {
+ // 父级没权限时继续看子级。只要子级有权限,父级也要保留,否则子菜单没地方挂。
const filteredChildren = filterAsyncRoutes(tmpRoute.children, permissions, userRoles, deviceType);
+
if (filteredChildren.length > 0) {
tmpRoute.children = filteredChildren;
- res.push(tmpRoute); // 即使父路由本身没有权限,只要有子路由有权限,也要保留父路由
+ res.push(tmpRoute);
}
}
- // 如果当前路由既没有权限,也没有有权限的子路由,则不添加到结果中
});
+
return res;
}
-// 新增:为普通用户添加订单管理和资源管理路由
+// 给普通用户和客户补充固定菜单:订单、资源,以及客户专属的工单/充值/发票等。
function addUserRoutes(routes, userType, orgType, userRoles = [], deviceType = 'pc') {
console.log("addUserRoutes - userType:", userType, "orgType:", orgType, "userRoles:", userRoles);
const userRoutes = [];
- // 修复:包含 orgType 为 2 和 3 的情况(公司客户和个人客户)
- if (userType === 'user' || orgType == 2 || orgType == 3) {
- const orderManagementRoute = routes.find(route => route.path === "/orderManagement");
- const resourceManagementRoute = routes.find(route => route.path === "/resourceManagement");
+ // orgType 为 2 或 3 时也按客户账号处理。
+ const isUserAccount = userType === 'user' || orgType == 2 || orgType == 3;
- // 新增:根据设备类型过滤
- if (orderManagementRoute && (deviceType === 'pc' || orderManagementRoute.meta?.isMobile === true)) {
- console.log("添加订单管理路由");
- userRoutes.push(JSON.parse(JSON.stringify(orderManagementRoute))); // 深拷贝
- }
+ if (isUserAccount) {
+ // 普通客户账号默认补订单管理和资源管理。
+ const baseUserRoutes = getRoutesByPath(routes, BASE_USER_ROUTE_PATHS)
+ .filter(route => isRouteAllowedByDevice(route, deviceType));
- if (resourceManagementRoute && (deviceType === 'pc' || resourceManagementRoute.meta?.isMobile === true)) {
- console.log("添加资源管理路由");
- userRoutes.push(JSON.parse(JSON.stringify(resourceManagementRoute))); // 深拷贝
- }
+ console.log("添加基础用户菜单路由:", baseUserRoutes.map(route => route.path));
+ userRoutes.push(...baseUserRoutes);
}
- // 新增:为所有用户添加五个新的客户菜单,但只有客户角色才能看到
- const newCustomerRoutes = [
- routes.find(route => route.path === "/unsubscribeManagement"),
- routes.find(route => route.path === "/informationPerfect"),
- routes.find(route => route.path === "/rechargeManagement"),
- routes.find(route => route.path === "/invoiceManagement"),
- routes.find(route => route.path === "/workOrderManagement")
- ].filter(route => {
- // 过滤掉undefined,并且只有客户角色才能看到这些路由
- return route && userRoles.includes('客户') &&
- (deviceType === 'pc' || route.meta?.isMobile === true);
- });
+ if (isCustomer(userRoles)) {
+ // 只有客户角色才补客户专属菜单。
+ const customerRoutes = getRoutesByPath(routes, CUSTOMER_EXTRA_ROUTE_PATHS)
+ .filter(route => isRouteAllowedByDevice(route, deviceType));
- console.log("添加新的客户菜单路由:", newCustomerRoutes.map(r => r.path));
- userRoutes.push(...newCustomerRoutes);
+ console.log("添加客户菜单路由:", customerRoutes.map(route => route.path));
+ userRoutes.push(...customerRoutes);
+ }
return userRoutes;
}
+// 运营角色额外补模型管理菜单,目前只在 PC 端展示。
+function addOperationRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
+ if (!userRoles.includes(OPERATION_ROLE) || deviceType !== 'pc') {
+ return accessedRoutes;
+ }
+
+ return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, OPERATION_EXTRA_ROUTE_PATHS));
+}
+
+// token市集是公共菜单,所有登录用户都要能看到。
+function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') {
+ const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS)
+ .filter(route => isRouteAllowedByDevice(route, deviceType));
+
+ return appendMissingRoutes(accessedRoutes, commonRoutes);
+}
+
+// 订单管理有两个特殊子菜单,只有 SPECIAL_ORDER_USER 能看到,其他用户过滤掉。
+function filterOrderChildrenByUser(routes, username) {
+ if (username === SPECIAL_ORDER_USER) {
+ console.log(`用户 ${username} 是 ${SPECIAL_ORDER_USER},保留所有订单子路由`);
+ return routes;
+ }
+
+ return routes.map(route => {
+ const nextRoute = cloneRoute(route);
+
+ // 找到订单管理后,移除特殊用户专属的子菜单。
+ if (nextRoute.path === '/orderManagement' && nextRoute.children) {
+ console.log(`用户 ${username} 不是 ${SPECIAL_ORDER_USER},过滤订单管理子路由`);
+ nextRoute.children = nextRoute.children.filter(child =>
+ !ORDER_CHILDREN_ONLY_FOR_SPECIAL_USER.includes(child.path)
+ );
+ console.log('过滤后订单子路由:', nextRoute.children.map(child => child.path));
+ }
+
+ if (nextRoute.children) {
+ // 子路由里如果还有订单管理,也继续递归处理。
+ nextRoute.children = filterOrderChildrenByUser(nextRoute.children, username);
+ }
+
+ return nextRoute;
+ });
+}
+
+// 整理后端权限列表。如果包含空 path,就按“拥有全部动态路由权限”处理。
+function getPermissionList(auths = []) {
+ const permissions = JSON.parse(JSON.stringify(auths));
+ const permissionPaths = permissions.map(item => item.path);
+
+ if (permissionPaths.includes('')) {
+ return getAllRoutePermissions(asyncRoutes);
+ }
+
+ return permissions;
+}
+
+// 根据后端 auths 生成第一版可访问路由。没有 auths 就不展示动态菜单。
+function getAccessedRoutesByPermission(auths, userRoles, deviceType) {
+ if (!auths.length) {
+ return [];
+ }
+
+ const permissions = getPermissionList(auths);
+ return filterAsyncRoutes(asyncRoutes, permissions, userRoles, deviceType);
+}
+
+// 判断是不是超级管理员账号:用户名包含 admin,并且不是客户组织。
+function isSuperAdminUser(username, orgType) {
+ return username && username.includes('admin') && orgType != 2 && orgType != 3;
+}
+
+// 超级管理员只拿超级管理员菜单;手机端不展示这个菜单。
+function getSuperAdminRoutes(deviceType) {
+ if (deviceType !== 'pc') {
+ return [];
+ }
+
+ return getRoutesByPath(asyncRoutes, [SUPER_ADMIN_ROUTE_PATH]).map(cloneRoute);
+}
+
+// 在已有权限菜单基础上,补充用户类型/客户角色需要固定展示的菜单。
+function addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType) {
+ const userSpecificRoutes = addUserRoutes(asyncRoutes, userType, orgType, userRoles, deviceType);
+ return appendMissingRoutes(accessedRoutes, userSpecificRoutes);
+}
+
const state = {
routes: [],
addRoutes: [],
@@ -192,12 +424,19 @@ const state = {
const mutations = {
SET_ROUTES: (state, routes) => {
console.log("MUTATION SET_ROUTES - received routes:", routes);
+ // addRoutes 只保存动态生成的菜单,方便 router.addRoutes 使用。
state.addRoutes = routes;
sessionStorage.setItem("routes", JSON.stringify(routes));
- // 将移动端首页路由也包含在内
+ // routes 是侧边栏最终读取的数据:基础路由 + 动态权限路由。
state.routes = constantRoutes.concat(routes);
console.log("MUTATION SET_ROUTES - final state.routes:", state.routes);
},
+ RESET_ROUTES: (state) => {
+ // 退出登录或切换账号时,必须清掉内存里的旧菜单,否则不刷新页面会继续显示上个角色的菜单。
+ state.routes = [];
+ state.addRoutes = [];
+ sessionStorage.removeItem("routes");
+ },
SETUSERS: (state, user) => {
state.users = user;
},
@@ -226,131 +465,46 @@ const actions = {
generateRoutes({ commit, rootState, state }, params) {
console.log("ACTION generateRoutes - params:", params);
return new Promise((resolve) => {
- let accessedRoutes;
-
- // 从参数或sessionStorage中获取用户类型和组织类型
+ // 1. 先拿到用户基础信息,优先用传进来的参数,没有就从 sessionStorage / vuex 兜底。
const userType = params.userType || sessionStorage.getItem('userType') || '';
const orgType = params.orgType || parseInt(sessionStorage.getItem('orgType')) || 0;
-
- // 获取用户角色(从store或sessionStorage)
- const userRoles = rootState.user.roles || JSON.parse(sessionStorage.getItem('roles') || '[]');
- console.log("用户角色:", userRoles);
-
- // 获取用户名
const username = params.user || rootState.user.user || '';
- console.log("当前用户名:", username, "检查是否是ZhipuHZ:", username === 'ZhipuHZ');
+ const userRoles = getCurrentRoles(params, rootState);
+ const deviceType = getDeviceType(state.isMobile);
+ const auths = params.auths ? JSON.parse(JSON.stringify(params.auths)) : [];
- console.log("用户类型:", userType, "orgType:", orgType);
+ // 2. 判断是不是超级管理员,超级管理员走单独菜单逻辑。
+ const isSuperAdmin = isSuperAdminUser(params.user, orgType);
- // 确定设备类型
- const deviceType = state.isMobile ? 'mobile' : 'pc';
- console.log("设备类型:", deviceType);
+ console.log("用户角色:", userRoles);
+ console.log("当前用户名:", username, `检查是否是${SPECIAL_ORDER_USER}:`, username === SPECIAL_ORDER_USER);
+ console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
+ console.log("ACTION generateRoutes - auths:", auths);
- // 修复:包含 orgType 为 2 和 3 的情况
- if (params.user && params.user.includes("admin") && orgType != 2 && orgType != 3) {
- // 管理员:只显示超级管理员菜单(仅PC端)
- if (deviceType === 'pc') {
- accessedRoutes = asyncRoutes.filter(item => item.path === '/superAdministrator');
- } else {
- accessedRoutes = [];
- }
- } else {
- const auths = params.auths ? JSON.parse(JSON.stringify(params.auths)) : [];
- console.log("ACTION generateRoutes - auths:", auths);
+ // 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。
+ let accessedRoutes = isSuperAdmin
+ ? getSuperAdminRoutes(deviceType)
+ : getAccessedRoutesByPermission(auths, userRoles, deviceType);
- if (auths.length) {
- // 确保 auths 中的 path 与路由 meta.fullPath 匹配
- const paths = auths.map((item) => {
- return item.path;
- });
- console.log("ACTION generateRoutes - paths from auths:", paths);
+ // 4. token市集是公共入口,所有登录用户都补上。
+ accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType);
- if (paths.includes("")) {
- // 如果权限列表包含空路径,认为用户有所有权限
- accessedRoutes = asyncRoutes || [];
- } else {
- // 传入用户角色和设备类型
- accessedRoutes = filterAsyncRoutes(asyncRoutes, auths, userRoles, deviceType);
- }
- } else {
- // 如果没有权限列表,不显示任何动态路由
- accessedRoutes = [];
- }
-
- // 为普通用户添加订单管理和资源管理路由以及新的五个客户菜单
+ if (!isSuperAdmin) {
+ // 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。
console.log("为用户添加特定路由");
- const userSpecificRoutes = addUserRoutes(asyncRoutes, userType, orgType, userRoles, deviceType);
-
- // 确保不重复添加路由,同时检查角色权限
- userSpecificRoutes.forEach(route => {
- const isCustomerRoute = [
- "/workOrderManagement", "/unsubscribeManagement", "/informationPerfect",
- "/rechargeManagement", "/invoiceManagement"
- ].includes(route.path);
-
- // 如果是客户路由但用户不是客户,则不添加
- if (isCustomerRoute && !userRoles.includes('客户')) {
- return;
- }
-
- if (!accessedRoutes.some(r => r.path === route.path)) {
- accessedRoutes.push(route);
- }
- });
-
+ accessedRoutes = addUserSpecificRoutes(accessedRoutes, userType, orgType, userRoles, deviceType);
console.log("添加用户特定路由后的accessedRoutes:", accessedRoutes);
}
- // ========== 暴力过滤:直接修改 accessedRoutes ==========
- // 遍历所有路由,找到 /orderManagement 路由,然后过滤它的子路由
- accessedRoutes = accessedRoutes.map(route => {
- if (route.path === "/orderManagement") {
- console.log("找到订单管理路由,准备过滤子路由,用户名:", username);
+ // 6. 运营角色额外补模型管理。
+ accessedRoutes = addOperationRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
- // 创建路由副本
- const newRoute = { ...route };
-
- if (newRoute.children) {
- // 如果不是 ZhipuHZ 用户,过滤掉 HistoricalOrders 和 orderDetails 路由
- if (username !== 'ZhipuHZ') {
- console.log(`用户 ${username} 不是 ZhipuHZ,过滤订单管理子路由`);
- newRoute.children = newRoute.children.filter(child =>
- child.path !== 'HistoricalOrders' && child.path !== 'orderDetails'
- );
- console.log(`过滤后子路由:`, newRoute.children.map(c => c.path));
- } else {
- console.log(`用户 ${username} 是 ZhipuHZ,保留所有子路由`);
- }
- }
-
- return newRoute;
- }
-
- // 对于其他路由,保持原样
- return route;
- });
-
- // 再次检查,确保没有遗漏的任何 orderManagement 路由
- accessedRoutes.forEach(route => {
- if (route.children) {
- route.children = route.children.filter(child => {
- // 如果子路由是 orderManagement,也需要处理
- if (child.path === "/orderManagement") {
- console.log("在子路由中找到订单管理路由,准备过滤,用户名:", username);
-
- if (child.children && username !== 'ZhipuHZ') {
- child.children = child.children.filter(grandChild =>
- grandChild.path !== 'HistoricalOrders' && grandChild.path !== 'orderDetails'
- );
- }
- }
- return true;
- });
- }
- });
+ // 7. 最后处理订单管理里的特殊子菜单权限。
+ accessedRoutes = filterOrderChildrenByUser(accessedRoutes, username);
console.log("ACTION generateRoutes - 最终 calculated accessedRoutes:", accessedRoutes);
+ // 8. 保存到 vuex 和 sessionStorage,侧边栏会读取 state.permission.routes。
commit("SET_ROUTES", accessedRoutes);
resolve(accessedRoutes);
});
diff --git a/f/web-kboss/src/store/modules/user.js b/f/web-kboss/src/store/modules/user.js
index 22b8fec..c91ee87 100644
--- a/f/web-kboss/src/store/modules/user.js
+++ b/f/web-kboss/src/store/modules/user.js
@@ -13,6 +13,13 @@ const safeToString = (value, defaultValue = '') => {
return value.toString();
};
+const normalizeLoginRoles = (roles) => {
+ if (!roles || roles === 'None') return [];
+ if (Array.isArray(roles)) return roles;
+ if (typeof roles === 'string') return roles.split(',').filter(Boolean);
+ return [];
+};
+
// 从sessionStorage恢复状态
const getStoredState = () => {
return {
@@ -130,8 +137,11 @@ const actions = {
// 修复:org_type 为 2 或 3 都表示客户
const userType = (org_type == 2 || org_type == 3) ? 'user' : 'admin';
- // 设置用户角色 - 如果是客户,则添加'客户'角色
- const userRoles = (org_type == 2 || org_type == 3) ? ['客户'] : ['管理员'];
+ // 使用接口返回的真实角色生成菜单;客户组织兜底补上“客户”角色。
+ const userRoles = normalizeLoginRoles(response.roles);
+ if ((org_type == 2 || org_type == 3) && !userRoles.includes('客户')) {
+ userRoles.push('客户');
+ }
commit("SET_USER_TYPE", userType);
// 确保 org_type 不为 undefined
@@ -141,6 +151,8 @@ const actions = {
console.log("登录用户类型:", userType, "org_type:", org_type, "用户角色:", userRoles);
data ? commit("SET_AUTHS", data) : commit("SET_AUTHS", []);
+ resetRouter();
+ commit("permission/RESET_ROUTES", null, { root: true });
const accessRoutes = await store.dispatch(
"permission/generateRoutes",
{
@@ -151,7 +163,6 @@ const actions = {
roles: userRoles // 新增:传递角色信息
}
)
- resetRouter();
router.addRoutes(accessRoutes);
resolve(response);
}
@@ -215,6 +226,7 @@ const actions = {
commit("SET_AUTHS", []);
removeToken();
resetRouter();
+ commit("permission/RESET_ROUTES", null, { root: true });
// 清除sessionStorage
sessionStorage.removeItem('user');
@@ -223,6 +235,8 @@ const actions = {
sessionStorage.removeItem('orgType');
sessionStorage.removeItem('mybalance');
sessionStorage.removeItem('roles'); // 新增:清除角色信息
+ sessionStorage.removeItem('juese');
+ sessionStorage.removeItem('jueseNew');
// reset visited views and cached views
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
@@ -243,6 +257,8 @@ const actions = {
commit("SET_USER", "");
commit("SET_AUTHS", []);
removeToken();
+ resetRouter();
+ commit("permission/RESET_ROUTES", null, { root: true });
// 清除sessionStorage
sessionStorage.removeItem('user');
@@ -250,6 +266,8 @@ const actions = {
sessionStorage.removeItem('userType');
sessionStorage.removeItem('orgType');
sessionStorage.removeItem('roles'); // 新增:清除角色信息
+ sessionStorage.removeItem('juese');
+ sessionStorage.removeItem('jueseNew');
resolve();
});
},
diff --git a/f/web-kboss/src/styles/sidebar.scss b/f/web-kboss/src/styles/sidebar.scss
index 8fd79e6..875de5f 100644
--- a/f/web-kboss/src/styles/sidebar.scss
+++ b/f/web-kboss/src/styles/sidebar.scss
@@ -25,7 +25,7 @@
// reset element-ui css
.horizontal-collapse-transition {
- transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ transition: width .28s ease, padding-left .28s ease, padding-right .28s ease;
}
.scrollbar-wrapper {
@@ -100,11 +100,11 @@
.hideSidebar {
.sidebar-container {
- width: 54px !important;
+ width: 64px !important;
}
.main-container {
- margin-left: 54px;
+ margin-left: 64px;
}
.submenu-title-noDropdown {
diff --git a/f/web-kboss/src/views/homePage/components/topBox/index.vue b/f/web-kboss/src/views/homePage/components/topBox/index.vue
index 6a4af0d..4de7b51 100644
--- a/f/web-kboss/src/views/homePage/components/topBox/index.vue
+++ b/f/web-kboss/src/views/homePage/components/topBox/index.vue
@@ -20,9 +20,9 @@
- 产品与服务
+ 基础云
- 模型广场
+ token市集
元境
@@ -116,7 +116,11 @@
-
-
+
{{
item.firTitle
}}
@@ -255,6 +259,7 @@ import store from "@/store";
import { getHomePath } from '@/views/setting/tools'
import MessageCenter from '@/components/MessageCenter/MessageCenter.vue'
import { reqAIChat } from '@/api/AI/ai'
+import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
export default Vue.extend({
name: "TopBox",
@@ -376,9 +381,85 @@ export default Vue.extend({
}
},
methods: {
- // 跳转元境 https://ai.opencomputing.cn/#/index
- goYuanjing() {
- window.open('https://ai.opencomputing.cn/#/index')
+ // 点击模型广场前校验登录状态
+ handleModelSquareClick() {
+ if (!this.loginState) {
+ this.$message.warning('请先登录哦~')
+ return
+ }
+ this.$router.push('/product')
+ },
+
+ // 跳转元境
+ async goYuanjing() {
+ if (!this.loginState) {
+ this.$message.warning('请先登录哦~')
+ return
+ }
+
+ const yuanJingWindow = window.open('', '_blank')
+
+ try {
+ const res = await gotoYuanJingAPI({
+ user_id: sessionStorage.getItem('userId')
+ })
+
+ const deerer = this.getYuanJingAuthorization(res)
+
+ if (!deerer) {
+ if (yuanJingWindow) {
+ yuanJingWindow.close()
+ }
+ this.$message.error((res && res.msg) || '获取元境授权参数失败')
+ return
+ }
+
+ const loginUrl = `https://ai.opencomputing.cn/#/getCookie?deerer=${encodeURIComponent(deerer)}`
+
+ if (yuanJingWindow) {
+ yuanJingWindow.location.href = loginUrl
+ } else {
+ window.location.href = loginUrl
+ }
+ } catch (error) {
+ if (yuanJingWindow) {
+ yuanJingWindow.close()
+ }
+ this.$message.error('跳转元境失败,请稍后重试')
+ }
+ },
+ getYuanJingAuthorization(res) {
+ if (!res) {
+ return ''
+ }
+
+ if (typeof res === 'string') {
+ return res
+ }
+
+ const data = res.data || res
+ if (typeof data === 'string') {
+ return data
+ }
+
+ return data.Authorization || data.authorization || data.token || data.header || data.value || ''
+ },
+ isPanelFirClickable(item) {
+ const title = (item && item.firTitle) || ''
+ return title === '元境' || title === 'TOKEN市集' || title === 'token市集'
+ },
+ handlePanelFirClick(item) {
+ const title = (item && item.firTitle) || ''
+ if (title === '元境') {
+ this.$store.commit('setShowHomeNav', false)
+ this.goYuanjing()
+ return
+ }
+
+ if (title === 'TOKEN市集' || title === 'token市集') {
+ this.$store.commit('setShowHomeNav', false)
+ this.handleModelSquareClick()
+ }
},
// 处理AI助手点击
handleAIClick() {
@@ -668,11 +749,17 @@ export default Vue.extend({
async logout() {
this.$store.commit('setLoginState', false)
store.commit('tagsView/resetBreadcrumbState');
+ store.commit('permission/RESET_ROUTES');
sessionStorage.removeItem("auths");
sessionStorage.removeItem("routes");
sessionStorage.removeItem("user");
sessionStorage.removeItem("userId");
sessionStorage.removeItem("org_type")
+ sessionStorage.removeItem("userType");
+ sessionStorage.removeItem("orgType");
+ sessionStorage.removeItem("roles");
+ sessionStorage.removeItem("juese");
+ sessionStorage.removeItem("jueseNew");
localStorage.removeItem("auths");
localStorage.removeItem("routes");
localStorage.removeItem("user");
diff --git a/f/web-kboss/src/views/homePage/ncmatch/mainPage/index.vue b/f/web-kboss/src/views/homePage/ncmatch/mainPage/index.vue
index 027a822..aa78a8b 100644
--- a/f/web-kboss/src/views/homePage/ncmatch/mainPage/index.vue
+++ b/f/web-kboss/src/views/homePage/ncmatch/mainPage/index.vue
@@ -1,58 +1,140 @@
-
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
NCMatch
-
算力供需对接平台
+
+
+
+
+
+
+
+
+
+
好用还省钱,Token 就上开元云
+
石景山OPC公共服务平台
+
+ 为 OPC 而生,极致性价比一站式模型平台
+
+
+
+
+
+
+
+
-
+
Token市集
+
汇聚海量精品模型,以更低成本畅享极致 AI 体验
+
+
+
+
+
+
+
+
+
+
创镜工坊
+
以文筑境,以镜生画,全场景 AI 影像创作
+
+
+
+
+
+
+
+
+
+
云枢基座
+
深耕基础云服务,筑牢 AI 平台数字根基
+
-
-
-
+
-
+
-
-
+ -->
@@ -91,6 +173,7 @@
import Vue from 'vue'
import { reqPublishProductSearchFirstPage, reqEnterpriseAuditInfoSearch, reqHomepageProductCategory, reqGetSupplyAndDemandSquareList } from '@/api/ncmatch'
import { mapGetters, mapState } from "vuex";
+import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
export default Vue.extend({
name: "mainPage",
@@ -153,6 +236,59 @@ export default Vue.extend({
this.showTip = false
this.$router.push('/customer/approve')
},
+ async goCreativeWorkshop() {
+ if (!this.loginState) {
+ this.$message.warning('请先登录哦~')
+ return
+ }
+
+ const yuanJingWindow = window.open('', '_blank')
+
+ try {
+ const res = await gotoYuanJingAPI({
+ user_id: sessionStorage.getItem('userId')
+ })
+
+ const deerer = this.getYuanJingAuthorization(res)
+
+ if (!deerer) {
+ if (yuanJingWindow) {
+ yuanJingWindow.close()
+ }
+ this.$message.error((res && res.msg) || '获取元境授权参数失败')
+ return
+ }
+
+ const loginUrl = `https://ai.opencomputing.cn/#/getCookie?deerer=${encodeURIComponent(deerer)}`
+
+ if (yuanJingWindow) {
+ yuanJingWindow.location.href = loginUrl
+ } else {
+ window.location.href = loginUrl
+ }
+ } catch (error) {
+ if (yuanJingWindow) {
+ yuanJingWindow.close()
+ }
+ this.$message.error('跳转元境失败,请稍后重试')
+ }
+ },
+ getYuanJingAuthorization(res) {
+ if (!res) {
+ return ''
+ }
+
+ if (typeof res === 'string') {
+ return res
+ }
+
+ const data = res.data || res
+ if (typeof data === 'string') {
+ return data
+ }
+
+ return data.Authorization || data.authorization || data.token || data.header || data.value || ''
+ },
handleCurrentChange(val) {
this.current_page = val
this.initData()
@@ -265,221 +401,579 @@ export default Vue.extend({
diff --git a/f/web-kboss/src/views/modelManagement/ApiDocument.vue b/f/web-kboss/src/views/modelManagement/ApiDocument.vue
new file mode 100644
index 0000000..62bba37
--- /dev/null
+++ b/f/web-kboss/src/views/modelManagement/ApiDocument.vue
@@ -0,0 +1,243 @@
+
+
+
+
+
+
+ MiniMax-M2.5 API 文档
+ 通过 OpenAI 兼容接口接入模型能力,支持对话生成、工具调用和流式输出。
+
+ {{ item }}
+
+
+
+
+ 1. 接口地址
+ 统一使用 HTTPS 请求,所有接口都需要携带平台签发的 API Key。
+ POST https://api.kboss.example.com/v2/chat/completions
+
+
+
+ 2. 模型能力列表
+
+
+
+
+
+
+ {{ scope.row.status }}
+
+
+
+
+
+
+
+
+
+ 4. 请求示例
+ {{ requestExample }}
+
+
+
+ 5. 返回示例
+ {{ responseExample }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/views/modelManagement/Experience.vue b/f/web-kboss/src/views/modelManagement/Experience.vue
new file mode 100644
index 0000000..c21828f
--- /dev/null
+++ b/f/web-kboss/src/views/modelManagement/Experience.vue
@@ -0,0 +1,546 @@
+
+
+
+
+
+
+
+
+
+
![logo]()
+
你好,我是模型体验助手
+
可以输入问题体验模型效果,也可以点击下方示例快速开始。
+
+
+
+
+
+
+
+
![logo]()
+
+
+
+
{{ message.role === 'assistant' ? '模型助手' : '我' }}
+
{{ message.content }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/views/modelManagement/ModelDetail.vue b/f/web-kboss/src/views/modelManagement/ModelDetail.vue
new file mode 100644
index 0000000..04f9165
--- /dev/null
+++ b/f/web-kboss/src/views/modelManagement/ModelDetail.vue
@@ -0,0 +1,349 @@
+
+
+
+
+
+ Token市集
+
+ 控制台
+ 用户后台
+
+
+
+
+
+
+
+
+
+
+
{{ modelInfo.name }}
+ 深度推理
+
+
+ 中文
+ 英文
+ 128K上下文
+
+
{{ modelInfo.description }}
+
+
+
+
+
+
版本介绍
+
+ 模型ID:{{ modelInfo.modelId }}
+
+
+
当前版本能力稳定,适合内容生成、知识问答、工具调用和复杂任务规划。
+
+
+ API文档
+ 体验
+
+
+
+
+
+
模型能力
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
模型限制
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+ 服务价格
+
+
+ 模型输入
+ 0.0021
+ 元/千Tokens
+
+
+ 模型输出
+ 0.0084
+ 元/千Tokens
+
+
+
+
+
+ 1. 模型介绍
+ {{ modelInfo.longDescription }}
+
+ 2. 模型亮点
+
+
+
+
推理
+
擅长处理数学、代码、逻辑分析和复杂任务拆解,适合作为业务助手和智能问答底座。
+
+
+
+
+
+
模型调优
+
提供稳定的上下文理解能力,便于后续结合业务数据进行知识增强和场景优化。
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/views/modelManagement/modelManagement.vue b/f/web-kboss/src/views/modelManagement/modelManagement.vue
new file mode 100644
index 0000000..2da7a2c
--- /dev/null
+++ b/f/web-kboss/src/views/modelManagement/modelManagement.vue
@@ -0,0 +1,580 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getModelId(scope.row) }}
+
+
+
+
+
+ {{ getModelDisplayName(scope.row) }}
+
+
+
+
+
+
+
+ {{ getModelType(scope.row) }}
+
+ -
+
+
+
+
+
+ {{ getProvider(scope.row) }}
+
+
+
+
+
+
+
输入价格: {{ formatPriceText(getInputPrice(scope.row)) }}
+
输出价格: {{ formatPriceText(getOutputPrice(scope.row)) }}
+
+
+
+
+
+
+
+ {{ scope.row.billing_method || '-' }}
+
+
+
+
+
+
+
+ {{ getUpdateTime(scope.row) }}
+
+
+
+
+
+
+ {{ getListingStatusText(scope.row.listing_status) }}
+
+
+
+
+
+
+
+ 置顶
+ 下移
+
+
+
+
+
+
+ 详情
+
+ 编辑
+ 上架
+
+
+ 下架
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/f/web-kboss/src/views/operation/operationReport/index.vue b/f/web-kboss/src/views/operation/operationReport/index.vue
new file mode 100644
index 0000000..688f2c8
--- /dev/null
+++ b/f/web-kboss/src/views/operation/operationReport/index.vue
@@ -0,0 +1,197 @@
+
+
+
+
+
+
+
活跃用户
+
{{ statCards.activeUsers }}
+
+
+
Token消耗
+
{{ statCards.tokenUsage }}
+
+
+
Tokens总费用
+
¥{{ statCards.totalFee }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.modelName }}
+
+
+
+
+
+
+
+
+
+ {{ scope.row.paymentMethod }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/f/web-kboss/src/views/product/allProduct/index.vue b/f/web-kboss/src/views/product/allProduct/index.vue
index 220e37a..5951174 100644
--- a/f/web-kboss/src/views/product/allProduct/index.vue
+++ b/f/web-kboss/src/views/product/allProduct/index.vue
@@ -1,5 +1,40 @@
+
@@ -8,7 +43,7 @@
class="nav-item"
:class="{ active: activeCategory === category.firTitle }"
@click="switchCategory(category)">
- {{ category.firTitle }}
+ {{ category.firTitle }}
@@ -16,7 +51,18 @@
@@ -177,12 +147,10 @@
import {
getCodeAPI, // 获取验证码API
getLogoAPI, // 获取Logo信息API
- // getPasswordCodeAPI, // 重置密码API
logintypeAPI, // 登录类型API
loginUserAPI, // 用户登录API
reqGetAppidAPI, // 获取微信AppID API
reqGetCodeAPI, // 获取微信授权码API
- // retrieveCodeAPI, // 获取重置密码验证码API,
} from "@/api/login";
import store from "@/store";
@@ -196,10 +164,11 @@ import { Message } from "element-ui";
import router, { resetRouter } from "@/router";
import { reqNewHomeFestival } from "@/api/newHome";
import { getHomePath } from '@/views/setting/tools'
+import ForgotPasswordDialog from './components/ForgotPasswordDialog.vue'
export default {
name: "indexNew",
- components: { BeforeLogin, promotionalInvitationCode },
+ components: { BeforeLogin, promotionalInvitationCode, ForgotPasswordDialog },
data() {
return {
// 微信登录相关
@@ -231,6 +200,7 @@ export default {
// 对话框和加载状态
loading: false, // 登录按钮加载状态
+ forgotPasswordVisible: false, // 忘记密码弹窗显示状态
// 登录表单数据
loginForm: {
@@ -258,21 +228,6 @@ export default {
capsTooltip: false,
passwordType: "password", // 密码输入框类型:password或text
- // 重置密码相关暂时注释
- // form: {
- // username: "", // 用户名
- // vcode: "", // 验证码
- // password: "", // 新密码
- // id: "", // 用户ID
- // codeid: "", // 验证码ID
- // },
-
- // forms: {
- // username: [{ required: true, message: "请输入姓名", trigger: "blur" }],
- // password: [{ required: true, message: "请输入密码", trigger: "blur" }],
- // vcode: [{ required: true, message: "请输入验证码", trigger: "blur" }],
- // },
-
// Logo显示控制
isLogo: false,
isShowSaleProduct: false, // 是否显示销售产品
@@ -374,20 +329,6 @@ export default {
}, 300);
},
- // 重置密码相关方法暂时注释
- // debouncedGetCode1: function () {
- // if (this.isDisabled1 || this.isGettingCode1) return;
- //
- // this.isGettingCode1 = true;
- //
- // clearTimeout(this.debounceTimer1);
- //
- // this.debounceTimer1 = setTimeout(() => {
- // this.getCode1();
- // this.isGettingCode1 = false;
- // }, 300);
- // },
-
// 跳转到百度产品页面
goBaidu(listUrl, url) {
this.$store.commit('setRedirectUrl', url)
@@ -747,54 +688,6 @@ export default {
});
},
- // getCode1() {
- // if (!this.form.username || !/^1[3-9]\d{9}$/.test(this.form.username)) {
- // this.$message.error('请输入正确的手机号');
- // return;
- // }
- //
- // retrieveCodeAPI({
- // mobile: this.form.username,
- // action_type: 'login'
- // }).then((res) => {
- // if (res.status == true) {
- // this.form.id = res.userid;
- // this.form.codeid = res.codeid;
- // let that = this;
- // this.time_count1 = 59;
- // this.isDisabled1 = true;
- // this.SendCode_text1 = "重新发送" + this.time_count1 + "s";
- //
- // if (!that.timer1) {
- // that.timer1 = setInterval(() => {
- // if (that.time_count1 > 0) {
- // that.time_count1--;
- // that.SendCode_text1 = "重新发送" + that.time_count1 + "s";
- // } else {
- // that.SendCode_text1 = "获取验证码";
- // clearInterval(that.timer1);
- // that.timer1 = null;
- // this.isDisabled1 = false;
- // that.time_count1 = 60;
- // }
- // }, 1000);
- // }
- // this.$message({
- // message: "验证码已发送,请注意查收。",
- // type: "success",
- // });
- // } else {
- // this.$message({
- // message: res.msg,
- // type: "error",
- // });
- // }
- // }).catch(error => {
- // this.isGettingCode1 = false;
- // this.$message.error('验证码获取失败');
- // });
- // },
-
// 标签页点击事件
handleClick(tab, event) {
console.log(tab, event);
@@ -932,50 +825,12 @@ export default {
});
},
- // resetPassword() {
- // this.dialogVisible = true;
- // this.$refs.loginForm.resetFields();
- // },
-
// 跳转到注册页面
handleRegister() {
console.log("注册按钮被点击了")
this.$router.push({ name: "registrationPage" });
},
- // cancelReset() {
- // this.dialogVisible = false;
- // this.$refs.form.resetFields();
- // },
-
- // handleSubmit() {
- // let parmas = {
- // id: this.form.id,
- // password: this.form.password,
- // codeid: this.form.codeid,
- // vcode: this.form.vcode,
- // };
- // getPasswordCodeAPI(parmas).then((res) => {
- // if (res.status == true) {
- // this.$message({
- // message: "密码重置成功",
- // type: "success",
- // });
- // this.isDisabled1 = false;
- // this.dialogVisible = false;
- // this.SendCode_text1 = "获取验证码";
- // clearInterval(this.timer1);
- // this.timer1 = null;
- // this.time_count1 = 60;
- // this.$refs.form.resetFields();
- // } else {
- // this.$message({
- // message: res.msg,
- // type: "error",
- // });
- // }
- // });
- // },
}
}
@@ -1163,10 +1018,22 @@ $dark_gray: #889aa4;
.two-btn {
width: 300px;
display: flex;
- justify-content: center;
+ justify-content: space-between;
+ align-items: center;
margin-top: 0;
}
+.forgot-password {
+ margin-bottom: 30px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
.go-register {
font-size: 14px;
color: #333;
diff --git a/f/web-kboss/src/views/modelManagement/ApiDocument.vue b/f/web-kboss/src/views/modelManagement/ApiDocument.vue
index 62bba37..85a394e 100644
--- a/f/web-kboss/src/views/modelManagement/ApiDocument.vue
+++ b/f/web-kboss/src/views/modelManagement/ApiDocument.vue
@@ -1,7 +1,7 @@
+
+
- MiniMax-M2.5 API 文档
- 通过 OpenAI 兼容接口接入模型能力,支持对话生成、工具调用和流式输出。
-
+
{{ apiDoc.model_name }} API 文档
+
{{ heroDescription }}
+
-
+
1. 接口地址
统一使用 HTTPS 请求,所有接口都需要携带平台签发的 API Key。
- POST https://api.kboss.example.com/v2/chat/completions
+ {{ apiUrlText }}
+
+
+
+
+
+
+
+ 2. 请求示例
+ {{ apiDoc.curl_code || requestExample }}
- 2. 模型能力列表
-
-
-
-
-
-
- {{ scope.row.status }}
-
-
-
-
+ 3. Python 示例
+ {{ apiDoc.python_code || pythonExample }}
-
-
- 4. 请求示例
- {{ requestExample }}
-
-
-
- 5. 返回示例
- {{ responseExample }}
-
-
-
- 6. 错误码
+ 4. 错误码
@@ -73,10 +61,22 @@
@@ -179,6 +340,14 @@ export default {
font-size: 13px;
}
+.token-market-link {
+ cursor: pointer;
+
+ &:hover {
+ color: #2f6bff;
+ }
+}
+
.detail-container {
width: 920px;
margin: 28px auto 0;
@@ -283,7 +452,7 @@ export default {
.price-list {
display: grid;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 18px;
}
@@ -312,6 +481,10 @@ export default {
&.output {
background: #fff0fa;
}
+
+ &.cache {
+ background: #f0fdf4;
+ }
}
.feature-block {
diff --git a/f/web-kboss/src/views/product/allProduct/index.vue b/f/web-kboss/src/views/product/allProduct/index.vue
index 5951174..1a7e311 100644
--- a/f/web-kboss/src/views/product/allProduct/index.vue
+++ b/f/web-kboss/src/views/product/allProduct/index.vue
@@ -1,41 +1,6 @@
-
-
+
+
+
+
+
+
+
{{ product.display_name || product.model_name }}
+
+
+
+
+ {{ product.model_type || '-' }}
+ {{ product.billing_method || '-' }}
+ {{ product.provider || '-' }}
+ {{ product.llmid }}
+
+
+ 输入 ¥{{ formatTokenPrice(product.input_token_price) }}/千Token
+ 输出 ¥{{ formatTokenPrice(product.output_token_price) }}/千Token
+
+
+ {{ getProviderInitial(product.provider) }}
+ {{ product.provider || '-' }}
+
+
+
+
+
+
+
+
+
-