Merge pull request 'revert 5309bff76966b9927677ee8d25c2deb5b2e9f078' (#144) from charles-patch-1 into prod

Reviewed-on: #144
This commit is contained in:
charles 2026-07-23 10:22:09 +08:00
commit 5487e12477
20 changed files with 225 additions and 1726 deletions

View File

@ -35,8 +35,6 @@ async def get_user_balance(ns={}):
}
orgid = await sor.R('organization', {'id': user[0]['orgid']})
balance = await getCustomerBalance(sor, orgid[0]['id'])
ns['balance'] = balance
debug(ns)
return {
'status': 'ok',
'balance': balance

View File

@ -1,94 +0,0 @@
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_logo, model_type,
context_length, input_token_price, output_token_price,
cache_hit_input_price, billing_method, billing_unit,
capabilities, limitations, highlights, description, sort_order,is_active, experience
"""
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_search1(params_kw)
return ret

View File

@ -3,38 +3,22 @@ def _escape(value):
return None
return str(value).replace("'", "''")
# 客户侧返回字段结构保持与原 model_management 版本一致
# 客户侧可见字段(不含 listing_status、is_active 等运营字段)
_CUSTOMER_MODEL_COLUMNS = """
id,
id AS llmid,
provider_name AS provider,
model AS model_name,
name AS display_name,
provider_iconurl AS model_logo,
catelogname AS model_type,
NULL AS context_length,
0 AS input_token_price,
0 AS output_token_price,
0 AS cache_hit_input_price,
pricing_display AS billing_method,
'' AS billing_unit,
description AS capabilities,
'' AS limitations,
'' AS highlights,
description,
0 AS sort_order,
1 AS is_active,
0 AS experience
id, llmid, provider, model_name, display_name, model_logo, model_type,
context_length, input_token_price, output_token_price,
cache_hit_input_price, billing_method, billing_unit,
capabilities, limitations, highlights, description, sort_order,is_active, experience
"""
def _customer_listed_conditions(ns):
"""已上架且启用的模型;支持按厂商、模型类别筛选"""
conditions = ["status = 'published'"]
conditions = ["listing_status = 1", "is_active = 1"]
if ns.get('provider'):
conditions.append("provider_name = '%s'" % _escape(ns.get('provider')))
conditions.append("provider = '%s'" % _escape(ns.get('provider')))
if ns.get('model_type'):
conditions.append("catelogname = '%s'" % _escape(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={}):
@ -60,27 +44,27 @@ async def model_management_customer_search(ns={}):
current_page = int(ns.get('current_page', 1))
offset = (current_page - 1) * page_size
where_clause = _customer_listed_conditions(ns)
listed_base = "status = 'published'"
listed_base = "listing_status = 1 AND is_active = 1"
db = DBPools()
async with db.sqlorContext('kboss') as sor:
try:
provider_sql = """
SELECT DISTINCT provider_name AS provider FROM llm
WHERE %s AND provider_name IS NOT NULL AND provider_name != ''
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 catelogname AS model_type FROM llm
WHERE %s AND catelogname IS NOT NULL AND catelogname != ''
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 llm WHERE %s;
SELECT COUNT(*) AS total_count FROM model_management WHERE %s;
""" % where_clause
find_sql = """
SELECT %s FROM llm
SELECT %s FROM model_management
WHERE %s
ORDER BY sort_order ASC
LIMIT %s OFFSET %s;

View File

@ -1,183 +1,6 @@
async def sync_model_to_llm(ns={}):
import aiohttp
async def get_sync_orgid(sor):
if ns.get('orgid'):
return ns.get('orgid')
user_li = await sor.R('users', {'id': await get_user()})
return user_li[0].get('orgid') if user_li else ''
def build_llm_data(item):
return {
'id': item.get('id') or uuid(),
'name': item.get('name'),
'model': item.get('model'),
'description': item.get('description'),
'iconid': item.get('iconid'),
'upappid': item.get('upappid'),
'providerid': item.get('providerid'),
'ownerid': item.get('ownerid') or '0',
'enabled_date': item.get('enabled_date'),
'expired_date': item.get('expired_date'),
'min_balance': item.get('min_balance'),
'status': item.get('status') or 'published',
'catelogname': item.get('catelogname'),
'catelog_id': item.get('catelog_id') or item.get('catelogid'),
'llmcatelogid': item.get('catelog_id') or item.get('catelogid'),
'apiname': item.get('apiname'),
'query_apiname': item.get('query_apiname'),
'query_period': item.get('query_period'),
'ppid': item.get('ppid'),
'provider_name': item.get('provider_name'),
'pricing_display': item.get('pricing_display'),
'provider_iconurl': item.get('provider_iconurl')
}
async def ensure_provider(sor, provider_orgcode, provider_name, sync_orgid, item):
exist_org_li = await sor.R('organization', {'orgcode': provider_orgcode, 'del_flg': '0'})
if exist_org_li:
provider_orgid = exist_org_li[0].get('id')
await sor.U('organization', {
'id': provider_orgid,
'orgname': provider_name,
'orgcode': provider_orgcode,
'parentid': sync_orgid,
'org_type': '4'
})
else:
provider_orgid = uuid()
await sor.C('organization', {
'id': provider_orgid,
'orgname': provider_name,
'contactor': '连战',
'contactor_phone': '13800138000',
'address': '北京市海淀区',
'main_business': provider_name,
'orgcode': provider_orgcode,
'emailaddress': 'lianzhan@cntoai.com',
'org_type': '4',
'parentid': sync_orgid
})
await openProviderAccounts(sor, sync_orgid, provider_orgid)
provider_li = await sor.R('provider', {'orgid': provider_orgid, 'del_flg': '0'})
provider_data = {
'name': provider_name,
'description': provider_name,
'appid': item.get('upappid') or '',
'providerkey': provider_orgcode,
'state': '1',
'start_date': '2026-07-01',
'end_date': '9999-12-31',
'settle_mode': '3',
'settle_flowid': '0',
'settle_datep': 'M00',
'discount_mode': 1,
'orgid': provider_orgid
}
if provider_li:
provider_id = provider_li[0].get('id')
provider_data['id'] = provider_id
await sor.U('provider', provider_data)
else:
provider_id = uuid()
provider_data['id'] = provider_id
await sor.C('provider', provider_data)
protocol_li = await sor.R('saleprotocol', {'offer_orgid': provider_orgid, 'bid_orgid': sync_orgid, 'del_flg': '0'})
if protocol_li:
protocolid = protocol_li[0].get('id')
await sor.U('saleprotocol', {
'id': protocolid,
'offer_orgid': provider_orgid,
'bid_orgid': sync_orgid,
'salemode': '0',
'settle_mode': '3',
'settle_dp': 'M00',
'start_date': '2026-07-01',
'end_date': '9999-12-31'
})
else:
protocolid = uuid()
await sor.C('saleprotocol', {
'id': protocolid,
'offer_orgid': provider_orgid,
'bid_orgid': sync_orgid,
'salemode': '0',
'settle_mode': '3',
'settle_dp': 'M00',
'start_date': '2026-07-01',
'end_date': '9999-12-31'
})
return {
'provider_orgid': provider_orgid,
'provider_id': provider_id,
'protocolid': protocolid
}
async def sync_product(sor, item, category_list, provider_orgid, protocolid, sync_orgid):
product_data = {
'providerid': provider_orgid,
'providerpid': item.get('id'),
'name': item.get('name'),
# 'description': item.get('description') or '',
'ptype': '21',
'state': '1',
'salemode': '0',
'product_code': item.get('model'),
'reseller_orgid': sync_orgid,
'classify': 'A',
'label': item.get('catelogname') or category_list.get('catelogname'),
# 'productgroup': item.get('catelog_id') or item.get('catelogid') or category_list.get('catelogid'),
'product_area': '北京',
'effect_date': '2026-07-01',
'expire_date': '9999-12-31'
}
product_li = await sor.R('product', {'providerid': provider_orgid, 'product_code': item.get('model'), 'del_flg': '0'})
if product_li:
product_id = product_li[0].get('id')
product_data['id'] = product_id
await sor.U('product', product_data)
product_action = 'update'
else:
product_id = uuid()
product_data['id'] = product_id
await sor.C('product', product_data)
product_action = 'insert'
salemode_data = {
'protocolid': protocolid,
'providerid': provider_orgid,
'productid': product_id,
'discount': 1,
'unit': 'hour'
}
product_salemode_li = await sor.R('product_salemode', {
'protocolid': protocolid,
'providerid': provider_orgid,
'productid': product_id,
'del_flg': '0'
})
if product_salemode_li:
salemode_data['id'] = product_salemode_li[0].get('id')
await sor.U('product_salemode', salemode_data)
else:
salemode_data['id'] = uuid()
await sor.C('product_salemode', salemode_data)
return product_action
async def upsert_llm(sor, item):
llm_data = build_llm_data(item)
exist_llm = await sor.R('llm', {'model': item.get('model')})
if exist_llm:
llm_data['id'] = exist_llm[0].get('id')
await sor.U('llm', llm_data)
return 'update'
await sor.C('llm', llm_data)
return 'insert'
# 从数据库读取domain和Bearer token
db = DBPools()
async with db.sqlorContext('kboss') as sor:
@ -190,13 +13,6 @@ async def sync_model_to_llm(ns={}):
}
domain = domain_li[0]['pvalue']
bearer_token = user_key[0]['pvalue']
# sync_orgid = await get_sync_orgid(sor)
sync_orgid = "mIWUHBeeDM8mwAFPIQ8pS" # 固定写
if not sync_orgid:
return {
'status': False,
'msg': '未找到当前用户所属机构'
}
url = f"{domain}/llmage/list_llms"
header = {
@ -216,93 +32,38 @@ async def sync_model_to_llm(ns={}):
# 插入数据库
db = DBPools()
async with db.sqlorContext('kboss') as sor:
provider_groups = {}
current_model_set = set()
new_llms_count = 0
new_llms_list = []
for category_list in result:
for item in category_list.get('llms', []):
provider_orgcode = item.get('providerid')
model = item.get('model')
if not provider_orgcode or not model:
# 查找数据库中是否已经存在,不存在就插入
exist_llm = await sor.R('llm', {'model': item.get('model')})
if exist_llm:
continue
item['catelogname'] = item.get('catelogname') or category_list.get('catelogname')
item['catelog_id'] = item.get('catelog_id') or item.get('catelogid') or category_list.get('catelogid')
current_model_set.add(model)
if provider_orgcode not in provider_groups:
provider_groups[provider_orgcode] = {
'provider_name': item.get('provider_name') or provider_orgcode or '模型供应商',
'items': []
new_llms = {
'id': item.get('id'),
'name': item.get('name'),
'model': item.get('model'),
'description': item.get('description'),
'llmcatelogid': item.get('catelog_id') or item.get('catelogid'),
'iconid': item.get('iconid'),
'upappid': item.get('upappid'),
'apiname': item.get('apiname'),
'providerid': item.get('providerid'),
'ownerid': item.get('ownerid') or '0',
'enabled_date': item.get('enabled_date'),
'expired_date': item.get('expired_date'),
'query_apiname': item.get('query_apiname') or '',
'query_period': item.get('query_period'),
'ppid': item.get('ppid'),
}
elif item.get('provider_name'):
provider_groups[provider_orgcode]['provider_name'] = item.get('provider_name')
provider_groups[provider_orgcode]['items'].append({
'item': item,
'category': category_list
})
provider_count = 0
product_insert_count = 0
product_update_count = 0
llm_insert_count = 0
llm_update_count = 0
synced_llms_list = []
for provider_orgcode, provider_group in provider_groups.items():
provider_name = provider_group.get('provider_name') or provider_orgcode or '模型供应商'
provider_data = await ensure_provider(
sor,
provider_orgcode,
provider_name,
sync_orgid,
provider_group['items'][0]['item']
)
provider_count += 1
for model_info in provider_group['items']:
item = model_info['item']
category_list = model_info['category']
product_action = await sync_product(
sor,
item,
category_list,
provider_data['provider_orgid'],
provider_data['protocolid'],
sync_orgid
)
if product_action == 'insert':
product_insert_count += 1
else:
product_update_count += 1
llm_action = await upsert_llm(sor, item)
if llm_action == 'insert':
llm_insert_count += 1
else:
llm_update_count += 1
synced_llms_list.append(item.get('model'))
llm_unpublished_count = 0
exist_llm_li = await sor.R('llm', {})
for exist_llm in exist_llm_li:
exist_model = exist_llm.get('model')
if exist_model and exist_model not in current_model_set and exist_llm.get('status') != 'unpublished':
await sor.U('llm', {
'id': exist_llm.get('id'),
'status': 'unpublished'
})
llm_unpublished_count += 1
new_llms_count += 1
new_llms_list.append(new_llms.get('model'))
await sor.C('llm', new_llms)
return {
'status': True,
'msg': 'sync_llm_list同步模型成功',
'data': {
'provider_count': provider_count,
'product_insert_count': product_insert_count,
'product_update_count': product_update_count,
'llm_insert_count': llm_insert_count,
'llm_update_count': llm_update_count,
'llm_unpublished_count': llm_unpublished_count,
'model_list': synced_llms_list
}
'msg': f"sync_llm_list同步模型成功共插入{new_llms_count}个模型,模型列表: {new_llms_list}"
}
except Exception as e:

145
b/llm.sql
View File

@ -1,145 +0,0 @@
# 创建表格命令
CREATE TABLE llm (
id VARCHAR(64) NOT NULL COMMENT '唯一标识',
name VARCHAR(50) NOT NULL COMMENT '模型名称',
model VARCHAR(50) NOT NULL COMMENT '模型代号',
description TEXT COMMENT '模型描述',
iconid VARCHAR(50) COMMENT '图标标识',
upappid VARCHAR(50) COMMENT '上层应用ID',
providerid VARCHAR(64) COMMENT '供应商ID',
ownerid VARCHAR(20) DEFAULT '0' COMMENT '所有者ID',
enabled_date DATE COMMENT '启用日期',
expired_date DATE COMMENT '过期日期',
min_balance INT DEFAULT 0 COMMENT '最低余额要求(元)',
status VARCHAR(20) COMMENT '状态如published',
catelogname VARCHAR(50) COMMENT '目录名称(如文生视频)',
catelog_id VARCHAR(20) COMMENT '目录ID',
apiname VARCHAR(50) COMMENT 'API名称',
query_apiname VARCHAR(50) COMMENT '查询API名称',
query_period INT COMMENT '查询周期(秒)',
ppid VARCHAR(64) COMMENT '定价计划ID',
provider_name VARCHAR(100) COMMENT '供应商名称',
pricing_display TEXT COMMENT '定价显示文本',
provider_iconurl VARCHAR(255) COMMENT '供应商图标URL',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='LLM模型信息表';
# 厂商返回的部分数据示例
[
{
"catelogid": "2i2v",
"catelogname": "首尾帧生视频",
"llms": [
{
"id": "r0ill7L0s6PtBS8mkmg-7",
"name": "viduq3-pro",
"model": "viduq3-pro",
"description": "vidu文生视频,viduq3-pro模型",
"iconid": "vidu",
"upappid": "vidu",
"providerid": "Bu3Z6OXANehbkD6OK5NJb",
"ownerid": "0",
"enabled_date": "2026-01-01",
"expired_date": "9999-12-31",
"min_balance": 10,
"status": "published",
"catelogname": "首尾帧生视频",
"catelog_id": "2i2v",
"apiname": "2i2v",
"query_apiname": "taskStatus",
"query_period": 30,
"ppid": "sF2gcl7UeANKtnZv8hfvL",
"provider_name": "北京生数科技有限公司",
"pricing_display": " - duration: 0.56 元/秒 ",
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=vidu"
}
]
},
{
"catelogid": "asr",
"catelogname": "语音识别",
"llms": [
{
"id": "9glv3S-JSzxholxAblU14",
"name": "qwen3-asr-flash",
"model": "qwen3-asr-flash",
"description": "通义千问系列的录音文件识",
"iconid": "qwen",
"upappid": "tongyi-wan",
"providerid": "6fadgewjraOyvxC_EkHou",
"ownerid": "0",
"enabled_date": "2025-12-01",
"expired_date": "9999-12-31",
"min_balance": 10,
"status": "published",
"catelogname": "语音识别",
"catelog_id": "asr",
"apiname": "short_asr",
"query_apiname": null,
"query_period": null,
"ppid": "16pwEsQeHiOrxC0O6A2j6",
"provider_name": "阿里云",
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
}
]
},
{
"catelogid": "digital_human",
"catelogname": "数字人",
"llms": [
{
"id": "KOsAo6YjTdU0KG7_sKUJF",
"name": "wan2.2-s2v",
"model": "wan2.2-s2v",
"description": "模型wan2.2-s2v输入音频和照片用照片中的人生成数字人",
"iconid": "qwen",
"upappid": "tongyi-wan",
"providerid": "6fadgewjraOyvxC_EkHou",
"ownerid": "0",
"enabled_date": "2025-12-19",
"expired_date": "9999-12-31",
"min_balance": 10,
"status": "published",
"catelogname": "数字人",
"catelog_id": "digital_human",
"apiname": "avator",
"query_apiname": "avatorstatus",
"query_period": 10,
"ppid": "0B6aldoAej1PpZ4ydtrEZ",
"provider_name": "阿里云",
"pricing_display": " - duration: 0.5 元/秒 [分辨率=480P]\n - duration: 0.9 元/秒 [分辨率=720P]",
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
}
]
},
{
"catelogid": "i2v",
"catelogname": "图生视频",
"llms": [
{
"id": "7y7kfYM0Xasizyi37WvAZ",
"name": "happyhorse-1.0-i2v",
"model": "happyhorse-1.0-i2v",
"description": "HappyHorse图生视频模型以首帧图片为基础支持通过文本描述进行引导生成物理真实、运动流畅的视频。",
"iconid": "qwen",
"upappid": "tongyi-wan",
"providerid": "6fadgewjraOyvxC_EkHou",
"ownerid": "0",
"enabled_date": "2026-04-28",
"expired_date": "9999-12-31",
"min_balance": 10,
"status": "published",
"catelogname": "图生视频",
"catelog_id": "i2v",
"apiname": "i2v-happyhorse",
"query_apiname": "t2vstatus",
"query_period": 10,
"ppid": "knEwDP35DEz5oMAXyvwN3",
"provider_name": "阿里云",
"pricing_display": " - duration: 0.9 元/秒 [SR=720]\n - duration: 1.6 元/秒 [SR=1080]",
"provider_iconurl": "https://token.opencomputing.cn/appbase/show_icon.dspy?id=qwen"
},
]
}
]

View File

@ -82,6 +82,14 @@ async def providerAdd(ns={}):
'status': False,
'msg': 'provider add failed, orgcode is empty'
}
# filter by orgcode
res = await sor.R('organization', {})
orgcodes = [orgc.get('orgcode') for orgc in res]
if org_code in orgcodes:
return {
'status': False,
'msg': 'provider add failed, orgcode has already exist'
}
if not ns.get('orgid'):
orgid_li = await sor.R('users', {'id': await get_user()})
orgid = orgid_li[0].get('orgid') if orgid_li else ''
@ -93,33 +101,8 @@ async def providerAdd(ns={}):
else:
orgid = ns.get('orgid')
# filter by orgcode; existing provider returns ids for idempotent callers
exist_org_li = await sor.R('organization', {'orgcode': org_code, 'del_flg': '0'})
if len(exist_org_li) >= 1:
exist_org = exist_org_li[0]
exist_provider_li = await sor.R('provider', {'orgid': exist_org.get('id'), 'del_flg': '0'})
if not exist_provider_li:
return {
'status': False,
'msg': 'provider add failed, orgcode has already exist but provider not found'
}
exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'bid_orgid': orgid, 'del_flg': '0'})
if not exist_protocol_li:
exist_protocol_li = await sor.R('saleprotocol', {'offer_orgid': exist_org.get('id'), 'del_flg': '0'})
return {
'status': True,
'msg': 'provider already exist',
'data': {
'orgid': exist_org.get('id'),
'provider_orgid': exist_org.get('id'),
'provider_id': exist_provider_li[0].get('id'),
'protocolid': exist_protocol_li[0].get('id') if exist_protocol_li else None
}
}
# 供应商parentid 和 运营机构id相同
ns_org['parentid'] = orgid
ns_sale_mode['bid_orgid'] = orgid
if not ns_provider.get('start_date'):
ns_provider['start_date'] = time.strftime('%Y-%m-%d %H:%M:%S')
@ -132,13 +115,7 @@ async def providerAdd(ns={}):
await openProviderAccounts(sor, orgid, ns_org['id'])
return {
"status": True,
"msg": "provider add success",
"data": {
"orgid": ns_org['id'],
"provider_orgid": ns_org['id'],
"provider_id": ns_provider['id'],
"protocolid": ns_sale_mode['id']
}
"msg": "provider add success"
}
except Exception as e:
raise e

View File

@ -27,16 +27,15 @@ async def reseller_add_user(ns):
try:
ns['id'] = uuid()
ns['password'] = password_encode(ns['password'])
# tenant_orgid = await get_reseller_user_tenant_orgid(sor, ns.get('orgid'))
tenant_orgid = ns.get('orgid')
tenant_orgid = await get_reseller_user_tenant_orgid(sor, ns.get('orgid'))
if not tenant_orgid:
return {'status': False, 'msg': '未找到用户所属租户'}
user_sql = """select id from users where username = '%s' and tenant_orgid = '%s' and del_flg = '0' limit 1;""" % (ns['username'], tenant_orgid)
user = await sor.sqlExe(user_sql, {})
if len(user) >= 1:
return {'status': False, 'msg': '用户名重复'}
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': ns.get('org_type')})
ns['user_reseller'] = ns.get('org_type')
rolereacs = await sor.R('role', {'role': '管理员', 'del_flg': '0', 'org_type': '1'})
ns['user_reseller'] = '1'
ns['tenant_orgid'] = tenant_orgid
await sor.C('users', ns)
await sor.C('userrole', {'userid': ns['id'], 'roleid': rolereacs[0]['id'], 'id': uuid()})

View File

@ -1,55 +0,0 @@
import json
def _parse_roleids(roleid):
if roleid is None:
return []
if isinstance(roleid, list):
return [i for i in roleid if i]
if isinstance(roleid, str):
roleid = roleid.strip()
if not roleid:
return []
try:
roleids = json.loads(roleid)
except Exception:
roleids = [roleid]
if isinstance(roleids, list):
return [i for i in roleids if i]
return [roleids] if roleids else []
return [roleid]
async def add_user_role(ns):
"""
用户分配角色
:param userid: 用户id
:param roleid: 角色id列表
:return:
"""
if not ns or not ns.get('userid') or 'roleid' not in ns:
return {'status': False, 'msg': '参数不正确'}
userid = ns.get('userid')
roleids = _parse_roleids(ns.get('roleid'))
db = DBPools()
async with db.sqlorContext('kboss') as sor:
userroles = await sor.R('userrole', {'userid': userid})
for userrole in userroles:
if userrole.get('roleid') not in roleids:
await sor.U('userrole', {'del_flg': '1', 'id': userrole['id']})
for roleid in roleids:
roles = {'userid': userid, 'roleid': roleid}
reacs = await sor.R('userrole', roles)
if len(reacs) >= 1:
await sor.U('userrole', {'id': reacs[0]['id'], 'del_flg': '0'})
continue
await sor.C('userrole', {'id': uuid(), 'userid': userid, 'roleid': roleid})
return {'status': True, 'msg': '添加成功'}
return {'status': False, 'msg': '添加失败'}
ret = await add_user_role(params_kw)
return ret

View File

@ -1,90 +0,0 @@
def _parse_page(ns, default_page=1, default_size=20, max_size=200):
try:
current_page = int(ns.get('current_page') or ns.get('page') or default_page)
except (TypeError, ValueError):
current_page = default_page
try:
page_size = int(ns.get('page_size') or default_size)
except (TypeError, ValueError):
page_size = default_size
current_page = max(1, current_page)
page_size = max(1, min(page_size, max_size))
return current_page, page_size, (current_page - 1) * page_size
async def get_user_and_role(ns={}):
"""获取机构下所有用户及用户对应的所有角色"""
orgid = ns.get('orgid')
if not orgid:
return {'status': False, 'msg': '缺少 orgid'}
current_page, page_size, offset = _parse_page(ns)
db = DBPools()
async with db.sqlorContext('kboss') as sor:
try:
where_sql = """
u.del_flg='0'
AND (
u.orgid=${orgid}$
OR o.parentid=${orgid}$
)
"""
if ns.get('username'):
where_sql += " AND u.username LIKE '%%%%%s%%%%'" % ns.get('username')
if ns.get('mobile'):
where_sql += " AND u.mobile LIKE '%%%%%s%%%%'" % ns.get('mobile')
count_rows = await sor.sqlExe(
"""
SELECT COUNT(*) AS total_count
FROM users u
LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0'
WHERE %s
""" % where_sql,
{'orgid': orgid},
)
total_count = count_rows[0]['total_count'] if count_rows else 0
sql = """
SELECT u.*
FROM users u
LEFT JOIN organization o ON u.orgid = o.id AND o.del_flg='0'
LEFT JOIN (
SELECT userid, MAX(create_at) AS userrole_create_at
FROM userrole
WHERE del_flg='0'
GROUP BY userid
) ur_sort ON u.id = ur_sort.userid
WHERE %s
ORDER BY ur_sort.userrole_create_at DESC, u.create_at DESC
LIMIT %d OFFSET %d
""" % (where_sql, page_size, offset)
users = await sor.sqlExe(sql, {'orgid': orgid})
for user in users:
user['roles'] = []
userroles = await sor.R('userrole', {'userid': user['id']})
userroles = sorted(userroles, key=lambda item: item.get('create_at') or '', reverse=True)
for userrole in userroles:
if userrole.get('del_flg') == '1' or not userrole.get('roleid'):
continue
roles = await sor.R('role', {'id': userrole['roleid']})
if len(roles) >= 1 and roles[0].get('del_flg') != '1':
user['roles'].append({
'id': roles[0].get('id'),
'name': roles[0].get('role'),
})
return {
'status': True,
'msg': 'ok',
'data': {
'total_count': total_count,
'current_page': current_page,
'page_size': page_size,
'rows': users,
},
}
except Exception as e:
return {'status': False, 'msg': '获取用户角色失败, %s' % str(e)}
ret = await get_user_and_role(params_kw)
return ret

View File

@ -4,10 +4,6 @@
<router-view/>
<FloatingBox
v-if="!$route.path.includes('kyyForm')&&!$route.path.includes('floatingBox')&&!$route.path.includes('screen')&&!$route.path.includes('wxPage')&&!$route.path.includes('wxDetailPage')"/>
<LoginDialog
:visible.sync="loginDialogVisible"
@success="handleGlobalLoginSuccess"
/>
<div>
<!-- 弹出通话提示-->
<div v-if="isShowVideo" class="is-accepted" style="padding: 5px;border-radius: 6px">
@ -42,7 +38,6 @@
<script>
import FloatingBox from './components/FloatingBox/FloatingBox.vue'; //
import LoginDialog from '@/views/LoginDialog/LoginDialog.vue'
import '@/assets/elementStyle.css'
import Hammer from 'hammerjs';
import {mapState} from "vuex";
@ -50,7 +45,6 @@ export default {
name: "App",
components: {
FloatingBox, //
LoginDialog,
},
data() {
return {
@ -58,8 +52,7 @@ export default {
socket: null,
userId: sessionStorage.getItem('userId'),
vedioObj: {},
customerName: '',
loginDialogVisible: false
customerName: ''
}
},
mounted() {
@ -73,10 +66,6 @@ export default {
}
this.setRemUnit()
this.initHammer();
window.addEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
},
beforeDestroy() {
window.removeEventListener('kboss-open-login-dialog', this.openGlobalLoginDialog)
},
computed: {
...mapState({
@ -86,16 +75,6 @@ export default {
})
},
methods: {
openGlobalLoginDialog() {
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
sessionStorage.setItem('loginRedirectPath', this.$route.fullPath)
}
this.loginDialogVisible = true
},
handleGlobalLoginSuccess() {
this.loginDialogVisible = false
this.userId = sessionStorage.getItem('userId')
},
playAudio() {
console.log("开始按钮点击了")
this.audioElement.play();

View File

@ -1,33 +0,0 @@
import request from '@/utils/request'
// 获取角色
export function getRoleAPI(data) {
return request({
url: `/role/getRole.dspy`,
method: 'get',
params: data
})
}
// 添加角色
export function addRoleAPI(data) {
return request({
url: `/reseller/reseller_add_user.dspy`,
method: 'post',
data: data
})
}
// 列表
export function getListAPI(data) {
return request({
url: `/user/get_user_and_role.dspy`,
method: 'get',
params: data
})
}
// 分配角色 参数为userid roleid
export function assignRoleAPI(data) {
return request({
url: `/user/add_user_role.dspy`,
method: 'post',
data: data
})
}

View File

@ -2305,13 +2305,6 @@ export const asyncRoutes = [
path: "/superAdministrator", component: Layout, redirect: "/superAdministrator/index", meta: {
title: "超级管理员", icon: "el-icon-user-solid", noCache: true, fullPath: "/superAdministrator",
}, children: [{
path: "roleManagement",
component: () => import("@/views/superAdministrator/roleManagement"),
name: "RoleManagement",
meta: {
title: "角色管理", fullPath: "/superAdministrator/roleManagement",
},
}, {
path: "addAdmin", component: () => import("@/views/superAdministrator/addAdmin"), name: "addAdmin", meta: {
title: "添加业主机构管理员", fullPath: "/superAdministrator/addAdmin",
},

View File

@ -7,7 +7,6 @@ const MOBILE_UA_REGEXP = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Ope
const CUSTOMER_ROLE = '客户';
const OPERATION_ROLE = '运营';
const FINANCE_ROLE = '财务';
const ADMINISTRATOR_ROLE = '管理员';
// 这个用户能看到订单管理里的特殊子菜单,比如历史订单和订单详情。
const SPECIAL_ORDER_USER = 'ZhipuHZ';
@ -24,8 +23,6 @@ const OPERATION_EXTRA_ROUTE_PATHS = ['/modelManagement', '/modelInfoConfig', '/o
// 财务角色需要额外补出来的菜单。
const FINANCE_EXTRA_ROUTE_PATHS = ['/financialOverview'];
const ADMINISTRATOR_ROUTE_FULL_PATH = '/superAdministrator/roleManagement';
// 普通客户账号默认要补出来的基础菜单。
const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement'];
@ -353,30 +350,6 @@ function addFinanceRoutes(accessedRoutes, routes, userRoles = [], deviceType = '
return appendMissingRoutes(accessedRoutes, getRoutesByPath(routes, FINANCE_EXTRA_ROUTE_PATHS));
}
function getAdministratorRoutes(routes, deviceType = 'pc') {
if (deviceType !== 'pc') {
return [];
}
const superAdminRoute = findRouteByPath(routes, SUPER_ADMIN_ROUTE_PATH);
if (!superAdminRoute) {
return [];
}
const clonedRoute = cloneRoute(superAdminRoute);
clonedRoute.redirect = ADMINISTRATOR_ROUTE_FULL_PATH;
clonedRoute.children = (clonedRoute.children || []).filter(child => child.meta?.fullPath === ADMINISTRATOR_ROUTE_FULL_PATH);
return clonedRoute.children.length ? [clonedRoute] : [];
}
function addAdministratorRoutes(accessedRoutes, routes, userRoles = [], deviceType = 'pc') {
if (!userRoles.includes(ADMINISTRATOR_ROLE)) {
return accessedRoutes;
}
return appendMissingRoutes(accessedRoutes, getAdministratorRoutes(routes, deviceType));
}
// token市集是公共菜单所有登录用户都要能看到。
function addCommonRoutes(accessedRoutes, routes, deviceType = 'pc') {
const commonRoutes = getRoutesByPath(routes, COMMON_ROUTE_PATHS)
@ -522,13 +495,6 @@ const actions = {
console.log("用户类型:", userType, "orgType:", orgType, "设备类型:", deviceType);
console.log("ACTION generateRoutes - auths:", auths);
if (!isSuperAdmin && userRoles.includes(ADMINISTRATOR_ROLE)) {
const administratorRoutes = getAdministratorRoutes(asyncRoutes, deviceType);
commit("SET_ROUTES", administratorRoutes);
resolve(administratorRoutes);
return;
}
// 3. 先生成第一版菜单:超级管理员只拿超管菜单,普通用户按后端 auths 过滤。
let accessedRoutes = isSuperAdmin
? getSuperAdminRoutes(deviceType)
@ -536,7 +502,6 @@ const actions = {
// 4. token市集是公共入口所有登录用户都补上。
accessedRoutes = addCommonRoutes(accessedRoutes, asyncRoutes, deviceType);
accessedRoutes = addAdministratorRoutes(accessedRoutes, asyncRoutes, userRoles, deviceType);
if (!isSuperAdmin) {
// 5. 普通用户再补一些固定入口,比如订单、资源、客户专属菜单。

View File

@ -165,8 +165,16 @@ service.interceptors.response.use(
isRedirectingToLogin = true
saveLoginRedirectPath()
showErrorMessage('登录已过期,请重新登录')
const currentPath = (router.currentRoute && router.currentRoute.path) || ''
const isHomePage = currentPath.includes('/homePage') || currentPath.includes('/ncmatchHome')
if (isHomePage) {
window.dispatchEvent(new Event('kboss-open-login-dialog'))
isRedirectingToLogin = false
} else {
router.push({path: '/login'}).finally(() => {
isRedirectingToLogin = false
})
}
}
// window.location.href = 'https://www.opencomputing.cn/'
} else if (code == 403) {

View File

@ -382,11 +382,6 @@ export default {
})
},
redirectAfterLogin(res) {
if (res.roles && res.roles.includes('管理员')) {
sessionStorage.removeItem('loginRedirectPath')
this.$router.push('/superAdministrator/roleManagement').catch(() => {})
return
}
const redirectPath = sessionStorage.getItem('loginRedirectPath')
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
sessionStorage.removeItem('loginRedirectPath')

View File

@ -544,7 +544,6 @@ export default Vue.extend({
else if (role.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing')
else if (role.includes('销售')) this.$router.push('/sales/distributorManagement')
else if (role.includes('财务')) this.$router.push('/finance/supplierSettlementStatistics')
else if (role.includes('管理员')) this.$router.push('/superAdministrator/roleManagement')
else if (role.includes('admin')) this.$router.push('/superAdministrator/addAdmin')
},
async logout() {

View File

@ -5,7 +5,7 @@
<i class="el-icon-arrow-left"></i>
返回
</button>
<span>元境 API 文档</span>
<span>API文档</span>
</header>
<main class="doc-container">
@ -19,12 +19,40 @@
></el-alert>
<section class="doc-hero">
<h1>元境 API 文档</h1>
<p>统一展示大模型视频图像音频任务查询等接口说明</p>
<h1>{{ apiDoc.model_name }} API 文档</h1>
<p>{{ heroDescription }}</p>
<!-- <div class="quick-tabs">
<span v-for="item in quickTabs" :key="item">{{ item }}</span>
</div> -->
</section>
<section v-loading="loading" class="doc-section markdown-section">
<div v-html="renderedMarkdown"></div>
<section v-loading="loading" class="doc-section">
<h2>1. 接口地址</h2>
<p>统一使用 HTTPS 请求所有接口都需要携带平台签发的 API Key</p>
<pre><code>{{ apiUrlText }}</code></pre>
</section>
<section class="doc-section">
<h2>2. 请求示例</h2>
<pre><code>{{ apiDoc.curl_code || requestExample }}</code></pre>
</section>
<section class="doc-section">
<h2>3. Python 示例</h2>
<pre><code>{{ apiDoc.python_code || pythonExample }}</code></pre>
</section>
<section class="doc-section">
<h2>4. 错误码</h2>
<el-table :data="errorCodes" border size="small">
<el-table-column prop="code" label="错误码" width="140"></el-table-column>
<el-table-column prop="message" label="说明"></el-table-column>
<el-table-column prop="suggestion" label="处理建议"></el-table-column>
</el-table>
</section>
</main>
@ -33,137 +61,124 @@
</template>
<script>
import { reqModelApiDocument } from '@/api/model/model'
export default {
name: 'ApiDocument',
data() {
return {
loading: false,
errorMessage: '',
markdownText: '',
markdownUrl: 'https://token.opencomputing.cn/docs/api_doc_zh.md'
apiDoc: {
id: '',
api_url: '',
model_id: '',
curl_code: '',
python_code: '',
model_name: '模型'
},
quickTabs: ['API概览', '认证方式', '请求参数', '代码示例', '错误码'],
capabilityTable: [
{ name: '文本对话', value: '支持单轮和多轮对话生成', status: '支持' },
{ name: '流式输出', value: '通过 stream=true 开启 SSE 增量返回', status: '支持' },
{ name: 'Function Call', value: '支持工具调用和结构化参数', status: '支持' },
{ name: '图像输入', value: '可在 messages 中传入图片内容', status: '支持' },
{ name: '私有化部署', value: '当前公共服务暂不支持私有化', status: '暂不支持' }
],
requestParams: [
{ name: 'model', type: 'string', required: '是', desc: '模型 ID例如 minimax-m2.5' },
{ name: 'messages', type: 'array', required: '是', desc: '对话消息列表,包含 role 和 content' },
{ name: 'temperature', type: 'number', required: '否', desc: '采样温度,数值越高输出越随机' },
{ name: 'stream', type: 'boolean', required: '否', desc: '是否开启流式返回' },
{ name: 'max_tokens', type: 'number', required: '否', desc: '限制模型最大输出长度' }
],
errorCodes: [
{ code: '401', message: '认证失败', suggestion: '检查 API Key 是否正确或过期' },
{ code: '429', message: '请求过于频繁', suggestion: '降低并发或等待限流恢复' },
{ code: '500', message: '服务异常', suggestion: '稍后重试或联系平台支持' }
],
requestExample: `curl https://api.kboss.example.com/v2/chat/completions \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer $KBOSS_API_KEY" \\
-d '{
"model": "minimax-m2.5",
"messages": [
{
"role": "user",
"content": "帮我写一段模型上架介绍"
}
],
"temperature": 0.7,
"stream": false
}'`,
pythonExample: `import requests
url = "https://api.kboss.example.com/v2/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer $KBOSS_API_KEY"
}
data = {
"model": "minimax-m2.5",
"messages": [{"role": "user", "content": "Hello!"}],
"stream": False
}
response = requests.post(url, headers=headers, json=data)
print(response.json())`
}
},
computed: {
renderedMarkdown() {
return this.renderMarkdown(this.markdownText)
apiUrlText() {
const curlUrl = this.getUrlFromCurl(this.apiDoc.curl_code)
const url = this.apiDoc.api_url || curlUrl
return url ? `POST ${url}` : '接口地址以后端返回为准'
},
heroDescription() {
const modelName = this.apiDoc.model_name || '当前模型'
return `${modelName} 接口调用示例,包含 curl 与 Python 两种接入方式。`
}
},
created() {
this.fetchMarkdownDocument()
this.fetchApiDocument()
},
watch: {
'$route.query.id'() {
this.fetchApiDocument()
}
},
methods: {
async fetchMarkdownDocument() {
// id API
async fetchApiDocument() {
const id = this.$route.query.id || this.$route.query.model_id
if (!id) {
this.errorMessage = '缺少模型ID无法获取 API 文档'
return
}
this.loading = true
this.errorMessage = ''
try {
const response = await fetch(this.markdownUrl)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
const res = await reqModelApiDocument({ id })
if (res && res.status && res.data) {
this.apiDoc = {
...this.apiDoc,
...res.data,
model_name: res.data.model_name || this.apiDoc.model_name
}
this.markdownText = await response.text()
return
}
this.errorMessage = (res && res.msg) || 'API 文档数据获取失败'
} catch (error) {
console.error('[API文档] 获取 Markdown 文档失败', error)
this.errorMessage = 'API 文档加载失败,请稍后重试'
console.error('[API文档] 获取模型 API 文档失败', error)
this.errorMessage = 'API 文档数据获取失败,请稍后重试'
} finally {
this.loading = false
}
},
escapeHtml(text = '') {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
},
renderInline(text = '') {
return this.escapeHtml(text)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
},
renderTable(lines) {
const rows = lines
.filter(line => line.trim().startsWith('|'))
.map(line => line.trim().replace(/^\||\|$/g, '').split('|').map(cell => this.renderInline(cell.trim())))
if (!rows.length) return ''
const header = rows[0] || []
const bodyRows = rows.slice(2)
return `<div class="md-table-wrap"><table><thead><tr>${header.map(cell => `<th>${cell}</th>`).join('')}</tr></thead><tbody>${bodyRows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
},
renderMarkdown(markdown = '') {
const lines = markdown.replace(/\r\n/g, '\n').split('\n')
const html = []
let index = 0
while (index < lines.length) {
const line = lines[index]
const trimmed = line.trim()
if (!trimmed) {
index += 1
continue
}
if (trimmed.startsWith('```')) {
const lang = trimmed.replace(/```/, '').trim()
const code = []
index += 1
while (index < lines.length && !lines[index].trim().startsWith('```')) {
code.push(lines[index])
index += 1
}
html.push(`<pre><code class="language-${this.escapeHtml(lang)}">${this.escapeHtml(code.join('\n'))}</code></pre>`)
index += 1
continue
}
if (trimmed.startsWith('|')) {
const tableLines = []
while (index < lines.length && lines[index].trim().startsWith('|')) {
tableLines.push(lines[index])
index += 1
}
html.push(this.renderTable(tableLines))
continue
}
if (/^---+$/.test(trimmed)) {
html.push('<hr>')
index += 1
continue
}
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/)
if (heading) {
const level = heading[1].length
html.push(`<h${level}>${this.renderInline(heading[2])}</h${level}>`)
index += 1
continue
}
if (/^[-*]\s+/.test(trimmed)) {
const items = []
while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^[-*]\s+/, ''))
index += 1
}
html.push(`<ul>${items.map(item => `<li>${this.renderInline(item)}</li>`).join('')}</ul>`)
continue
}
if (/^\d+\.\s+/.test(trimmed)) {
const items = []
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^\d+\.\s+/, ''))
index += 1
}
html.push(`<ol>${items.map(item => `<li>${this.renderInline(item)}</li>`).join('')}</ol>`)
continue
}
html.push(`<p>${this.renderInline(trimmed)}</p>`)
index += 1
}
return html.join('')
getUrlFromCurl(curlCode) {
if (!curlCode) return ''
const match = String(curlCode).match(/https?:\/\/[^\s\\]+/)
return match ? match[0] : ''
},
goBack() {
this.$router.back()
@ -260,103 +275,8 @@ export default {
}
}
.markdown-section {
line-height: 1.8;
::v-deep h1 {
margin: 0 0 24px;
color: #111827;
font-size: 30px;
font-weight: 800;
}
::v-deep h2 {
margin: 34px 0 16px;
padding-left: 10px;
color: #1f2d3d;
font-size: 22px;
border-left: 3px solid #2f6bff;
}
::v-deep h3 {
margin: 26px 0 12px;
color: #1f2937;
font-size: 18px;
}
::v-deep h4,
::v-deep h5 {
margin: 20px 0 10px;
color: #334155;
font-size: 16px;
}
::v-deep p,
::v-deep li {
color: #475569;
font-size: 14px;
}
::v-deep ul,
::v-deep ol {
padding-left: 22px;
}
::v-deep hr {
height: 1px;
margin: 28px 0;
background: #e5e7eb;
border: 0;
}
::v-deep code {
padding: 2px 6px;
color: #d6336c;
background: #fff1f2;
border-radius: 5px;
}
::v-deep pre code {
padding: 0;
color: inherit;
background: transparent;
border-radius: 0;
}
::v-deep .md-table-wrap {
width: 100%;
margin: 16px 0 24px;
overflow-x: auto;
border: 1px solid #e5e7eb;
border-radius: 10px;
}
::v-deep table {
width: 100%;
min-width: 680px;
border-collapse: collapse;
background: #fff;
}
::v-deep th,
::v-deep td {
padding: 10px 12px;
color: #475569;
font-size: 13px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
border-right: 1px solid #e5e7eb;
}
::v-deep th {
color: #1f2937;
font-weight: 700;
background: #f8fafc;
}
}
pre {
margin: 14px 0 24px;
margin: 0;
padding: 16px;
color: #e6edf3;
overflow-x: auto;

View File

@ -112,20 +112,13 @@
v-for="(product, index) in displayedTokenProducts"
:key="product.id"
class="token-market-card"
@click="goModelDetail(product)"
>
<!-- @click="goModelDetail(product)" -->
<div class="token-card-top">
<span class="token-provider-avatar">
<img
v-if="product.model_logo && !product.logoLoadFailed"
:src="getModelLogoUrl(product.model_logo)"
alt=""
@error="handleModelLogoError(product)"
>
<img v-if="product.model_logo" :src="getModelLogoUrl(product.model_logo)" alt="">
<span v-else>{{ getProviderInitial(product.provider || product.display_name || product.model_name) }}</span>
</span>
<div class="token-title-group">
<h3>{{ product.display_name || product.model_name }}</h3>
<p>{{ product.provider || product.model_name || '-' }}</p>
@ -133,14 +126,25 @@
</div>
<div class="token-price-grid">
<div
v-for="(price, priceIndex) in parseBillingMethod(product.billing_method)"
:key="`${product.id}-billing-${priceIndex}`"
class="token-price-item"
>
<span>{{ price.name }}</span>
<strong>{{ price.price }}</strong>
<em>{{ price.desc }}</em>
<div class="token-price-item">
<span>输入</span>
<strong>¥{{ formatTokenPrice(product.input_token_price) }}</strong>
<em>/百万 tokens</em>
</div>
<div class="token-price-item">
<span>输出</span>
<strong>¥{{ formatTokenPrice(product.output_token_price) }}</strong>
<em>/百万 tokens</em>
</div>
<div class="token-price-item">
<span>缓存读</span>
<strong>¥{{ formatTokenPrice(product.cache_hit_input_price) }}</strong>
<em>/百万 tokens</em>
</div>
<div class="token-price-item">
<span>缓存创建</span>
<strong>¥{{ formatTokenPrice(product.input_token_price) }}</strong>
<em>/百万 tokens</em>
</div>
</div>
@ -391,28 +395,6 @@ export default {
return num.toFixed(4).replace(/\.?0+$/, '');
},
parseBillingMethod(value) {
const text = String(value || '').trim();
if (!text) {
return [{ name: '计费方式', price: '按量计费', desc: '详情以后端配置为准' }];
}
return text
.split(/\n+/)
.map(line => line.replace(/^\s*[-–—]\s*/, '').trim())
.filter(Boolean)
.map(line => {
const match = line.match(/^([^:]+)[:]\s*([^\[]+)(?:\[(.+)\])?$/);
if (!match) {
return { name: '计费说明', price: line, desc: '' };
}
return {
name: match[1].trim(),
price: match[2].trim(),
desc: (match[3] || '').trim()
};
});
},
// logo
getProviderInitial(provider) {
return provider ? provider.slice(0, 1) : 'M';
@ -426,10 +408,6 @@ export default {
return `${this.IMG_URL}${modelLogo}`;
},
handleModelLogoError(product) {
this.$set(product, 'logoLoadFailed', true);
},
normalizeTokenConfigList(value) {
if (!value) return [];
let parsedValue = value;
@ -1493,29 +1471,13 @@ export default {
.token-price-grid {
position: relative;
z-index: 1;
max-height: 178px;
display: flex;
flex-direction: column;
gap: 5px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 12px;
margin-bottom: 10px;
padding-right: 4px;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.token-price-item {
padding: 7px 9px;
background: rgba(248, 250, 252, 0.86);
border: 1px solid #edf1f7;
border-radius: 10px;
span,
em {
display: block;
@ -1526,24 +1488,19 @@ export default {
span {
margin-bottom: 2px;
font-size: 11px;
font-weight: 700;
}
strong {
display: block;
color: #0f172a;
font-size: 13px;
color: #111827;
font-size: 15px;
font-weight: 800;
line-height: 1.25;
line-height: 1.1;
}
em {
margin-top: 2px;
margin-top: 0;
font-size: 10px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
@ -1601,8 +1558,8 @@ export default {
img {
display: block;
width: 34px;
height: 34px;
width: 100%;
height: 100%;
object-fit: cover;
}
}

View File

@ -1,619 +0,0 @@
<template>
<div class="role-management-page">
<div class="page-head">
<div>
<p class="eyebrow">ROLE MANAGEMENT</p>
<h1>角色管理</h1>
<span>统一管理系统用户与角色权限</span>
</div>
<el-button type="primary" icon="el-icon-plus" class="add-role-btn" @click="openAddDialog">
添加角色
</el-button>
</div>
<div class="filter-card">
<el-form :model="queryForm" inline class="filter-form">
<el-form-item label="用户名">
<el-input
v-model.trim="queryForm.username"
size="small"
clearable
placeholder="请输入用户名"
@keyup.enter.native="handleSearch"
/>
</el-form-item>
<el-form-item label="手机号">
<el-input
v-model.trim="queryForm.mobile"
size="small"
clearable
placeholder="请输入手机号"
@keyup.enter.native="handleSearch"
/>
</el-form-item>
<el-form-item>
<el-button size="small" icon="el-icon-search" type="primary" @click="handleSearch">搜索</el-button>
<el-button size="small" icon="el-icon-refresh-left" @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-card">
<el-table v-loading="loading" :data="roles" class="role-table" empty-text="暂无角色数据">
<el-table-column label="序号" width="80" align="center">
<template slot-scope="scope">{{ scope.$index + 1 }}</template>
</el-table-column>
<el-table-column prop="username" label="用户名" min-width="180">
<template slot-scope="scope">
<div class="role-name-cell">
<span class="role-avatar">{{ getAvatarText(scope.row.username) }}</span>
<div>
<strong>{{ scope.row.username }}</strong>
<small>{{ getRoleText(scope.row.roles) }}</small>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="email" label="邮箱" min-width="220" show-overflow-tooltip />
<el-table-column prop="mobile" label="手机号" width="150" align="center" />
<el-table-column label="角色权限" width="180" align="center">
<template slot-scope="scope">
{{ getRoleText(scope.row.roles) }}
</template>
</el-table-column>
<el-table-column prop="create_at" label="创建时间" width="180" align="center" />
<el-table-column label="操作" width="140" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openAssignDialog(scope.row)">分配角色</el-button>
<!-- <el-button type="text" size="mini" class="danger-text">删除</el-button> -->
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<span class="pagination-info"> {{ total }} </span>
<el-pagination
background
layout="prev, pager, next, jumper"
:current-page.sync="currentPage"
:page-size="pageSize"
:total="total"
@current-change="handlePageChange"
/>
</div>
</div>
<el-dialog
:visible.sync="addDialogVisible"
append-to-body
width="520px"
custom-class="role-add-dialog"
:close-on-click-modal="false"
@close="resetAddForm"
>
<div slot="title" class="dialog-title">
<div class="dialog-icon">
<i class="el-icon-user-solid"></i>
</div>
<div>
<h3>添加角色用户</h3>
<p>创建角色账号并配置对应角色权限</p>
</div>
</div>
<el-form ref="addForm" :model="addForm" :rules="addRules" label-position="top" class="dialog-form">
<el-form-item label="用户名" prop="username">
<el-input v-model.trim="addForm.username" maxlength="30" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model.trim="addForm.password" type="password" show-password maxlength="30" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model.trim="addForm.email" maxlength="60" placeholder="请输入邮箱" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model.trim="addForm.mobile" maxlength="11" placeholder="请输入手机号" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" class="submit-btn" @click="submitAddRole()">确认添加</el-button>
</div>
</el-dialog>
<el-dialog
:visible.sync="assignDialogVisible"
append-to-body
width="500px"
custom-class="role-add-dialog"
:close-on-click-modal="false"
@close="resetAssignForm"
>
<div slot="title" class="dialog-title">
<div class="dialog-icon">
<i class="el-icon-s-check"></i>
</div>
<div>
<h3>分配角色</h3>
<p> {{ currentAssignUser.username || '当前用户' }} 配置可用角色权限</p>
</div>
</div>
<el-form ref="assignForm" :model="assignForm" :rules="assignRules" label-position="top" class="dialog-form">
<el-form-item label="角色权限" prop="roleid">
<el-select
v-model="assignForm.roleid"
multiple
collapse-tags
placeholder="请选择角色权限"
class="full-select"
filterable
>
<el-option
v-for="item in roleOptions"
:key="item.id"
:label="item.role"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="assignDialogVisible = false">取消</el-button>
<el-button type="primary" class="submit-btn" @click="submitAssignRole">确认分配</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { addRoleAPI, assignRoleAPI, getListAPI, getRoleAPI } from '@/api/roleManagement';
export default {
name: 'RoleManagement',
data() {
return {
queryForm: {
username: '',
mobile: ''
},
loading: false,
currentPage: 1,
pageSize: 10,
total: 0,
addDialogVisible: false,
assignDialogVisible: false,
currentAssignUser: {},
addForm: this.createEmptyRole(),
assignForm: {
roleid: []
},
roleOptions: [],
addRules: {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
],
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
]
},
assignRules: {
roleid: [{ required: true, type: 'array', message: '请选择角色权限', trigger: 'change' }]
},
roles: []
}
},
created() {
this.getRoleList()
this.getUserList()
},
methods: {
createEmptyRole() {
return {
username: '',
password: '',
email: '',
mobile: ''
}
},
openAddDialog() {
if (!this.roleOptions.length) this.getRoleList()
this.addDialogVisible = true
},
//
async getRoleList() {
const orgType = sessionStorage.getItem('org_type') || sessionStorage.getItem('orgType') || ''
try {
const res = await getRoleAPI({ org_type: orgType })
if (res && res.status === true) {
this.roleOptions = this.normalizeRoleOptions(res.data)
return
}
this.roleOptions = []
} catch (error) {
this.roleOptions = []
this.$message.error('角色权限获取失败')
}
},
handleSearch() {
this.currentPage = 1
this.getUserList()
},
resetSearch() {
this.queryForm = {
username: '',
mobile: ''
}
this.currentPage = 1
this.getUserList()
},
handlePageChange(page) {
this.currentPage = page
this.getUserList()
},
getAvatarText(username) {
return String(username || '-').slice(0, 1).toUpperCase()
},
getRoleText(roles) {
return Array.isArray(roles) && roles.length
? roles.map(item => item.name).join('、')
: '-'
},
normalizeRoleOptions(list) {
return Array.isArray(list)
? list.map(item => ({
...item,
role: item.role || item.name || ''
}))
: []
},
mergeRoleOptions(roles = []) {
const nextOptions = [...this.roleOptions]
roles.forEach(role => {
if (role && role.id && !nextOptions.some(item => item.id === role.id)) {
nextOptions.push({
id: role.id,
role: role.name || role.role || role.id
})
}
})
this.roleOptions = nextOptions
},
getOrgId() {
return sessionStorage.getItem('orgid') || ''
},
async getUserList() {
this.loading = true
try {
const res = await getListAPI({
orgid: this.getOrgId(),
current_page: this.currentPage,
page_size: this.pageSize,
username: this.queryForm.username,
mobile: this.queryForm.mobile
})
if (res && res.status === true && res.data) {
this.roles = Array.isArray(res.data.rows) ? res.data.rows : []
this.total = Number(res.data.total_count || 0)
this.currentPage = Number(res.data.current_page || this.currentPage)
this.pageSize = Number(res.data.page_size || this.pageSize)
return
}
this.roles = []
this.total = 0
} catch (error) {
this.roles = []
this.total = 0
this.$message.error('用户角色列表获取失败')
} finally {
this.loading = false
}
},
resetAddForm() {
this.addForm = this.createEmptyRole()
this.$nextTick(() => {
this.$refs.addForm && this.$refs.addForm.clearValidate()
})
},
resetAssignForm() {
this.currentAssignUser = {}
this.assignForm = {
roleid: []
}
this.$nextTick(() => {
this.$refs.assignForm && this.$refs.assignForm.clearValidate()
})
},
submitAddRole() {
this.$refs.addForm.validate(async valid => {
if (!valid) return
await this.addUser()
})
},
//
async addUser() {
const orgType = sessionStorage.getItem('org_type') || sessionStorage.getItem('orgType') || ''
const data = {
username: this.addForm.username,
password: this.addForm.password,
email: this.addForm.email,
mobile: this.addForm.mobile,
orgid: this.getOrgId(),
org_type: orgType
}
try {
const res = await addRoleAPI(data)
if (res && res.status === true) {
const now = new Date()
const createdAt = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
this.roles.unshift({
username: this.addForm.username,
email: this.addForm.email,
mobile: this.addForm.mobile,
roles: [],
create_at: createdAt
})
this.total += 1
this.$message.success(res.msg || '角色用户添加成功')
this.addDialogVisible = false
this.getUserList()
return
}
this.$message.error((res && res.msg) || '角色用户添加失败')
} catch (error) {
this.$message.error('角色用户添加失败')
}
},
async openAssignDialog(row) {
this.currentAssignUser = row || {}
if (!this.roleOptions.length) await this.getRoleList()
this.mergeRoleOptions(row.roles || [])
this.assignForm.roleid = Array.isArray(row.roles) ? row.roles.map(item => item.id) : []
this.assignDialogVisible = true
},
submitAssignRole() {
this.$refs.assignForm.validate(async valid => {
if (!valid) return
try {
const res = await assignRoleAPI({
userid: this.currentAssignUser.id,
roleid: this.assignForm.roleid
})
if (res && res.status === true) {
this.$message.success(res.msg || '角色分配成功')
this.assignDialogVisible = false
this.getUserList()
return
}
this.$message.error((res && res.msg) || '角色分配失败')
} catch (error) {
this.$message.error('角色分配失败')
}
})
}
}
}
</script>
<style scoped lang="scss">
.role-management-page {
min-height: calc(100vh - 84px);
padding: 24px;
background: #f3f7ff;
}
.page-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.eyebrow {
margin: 0 0 6px;
color: #3b82f6;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
.page-head h1 {
margin: 0;
color: #111827;
font-size: 28px;
font-weight: 800;
}
.page-head span {
display: inline-block;
margin-top: 8px;
color: #64748b;
font-size: 14px;
}
.add-role-btn {
height: 38px;
border-radius: 10px;
}
.filter-card,
.table-card {
background: #fff;
border: 1px solid #edf0f5;
border-radius: 16px;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.04);
}
.filter-card {
padding: 16px;
margin-bottom: 18px;
}
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: center;
}
.filter-form ::v-deep .el-form-item {
margin-bottom: 0;
}
.filter-form ::v-deep .el-input__inner {
border-radius: 9px;
}
.filter-form ::v-deep .el-button--small {
height: 32px;
border-radius: 9px;
}
.table-card {
overflow: hidden;
}
.table-pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-top: 1px solid #edf0f5;
}
.pagination-info {
color: #64748b;
font-size: 14px;
}
.role-table ::v-deep th {
background: #f8fafc;
color: #64748b;
font-weight: 700;
}
.role-name-cell {
display: flex;
align-items: center;
gap: 12px;
}
.role-avatar {
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #2563eb;
font-size: 16px;
font-weight: 800;
background: #eff6ff;
border-radius: 10px;
}
.role-name-cell strong {
display: block;
color: #1f2937;
font-size: 14px;
}
.role-name-cell small {
display: block;
margin-top: 3px;
color: #94a3b8;
font-size: 12px;
}
.danger-text {
color: #ef4444;
}
::v-deep .role-add-dialog {
border-radius: 24px;
overflow: hidden;
}
::v-deep .role-add-dialog .el-dialog__header {
padding: 26px 30px 18px;
border-bottom: 1px solid #edf0f5;
}
::v-deep .role-add-dialog .el-dialog__body {
padding: 24px 30px 6px;
}
::v-deep .role-add-dialog .el-dialog__footer {
padding: 16px 30px 28px;
}
.dialog-title {
display: flex;
align-items: center;
gap: 14px;
}
.dialog-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
color: #2563eb;
font-size: 20px;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border-radius: 14px;
}
.dialog-title h3 {
margin: 0;
color: #111827;
font-size: 22px;
font-weight: 800;
}
.dialog-title p {
margin: 5px 0 0;
color: #64748b;
font-size: 13px;
}
.dialog-form ::v-deep .el-form-item {
margin-bottom: 18px;
}
.dialog-form ::v-deep .el-form-item__label {
padding-bottom: 7px;
color: #334155;
font-weight: 700;
}
.dialog-form ::v-deep .el-input__inner,
.dialog-form ::v-deep .el-textarea__inner {
border-radius: 12px;
}
.full-select {
width: 100%;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-footer .el-button {
min-width: 96px;
height: 38px;
border-radius: 10px;
}
.submit-btn {
background: #2563eb;
border-color: #2563eb;
}
</style>