From bdbf989b5e7488086f9c7e421e3be03354350a7a Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Tue, 1 Sep 2026 11:47:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=88=E8=AE=A1=E4=BB=B7=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=89=E9=A1=B5=E9=9D=A2=E4=B8=8E=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 左侧计价对象枚举列表(工作流 / 模型 / 业务模块,模型支持类型筛选与配置状态圆点) - 右侧按对象类型切换工作流 / 模型 / 业务编辑器,费率规则编辑与后端配置回显 - 新增 src/api/trade/pricing 接口(subjects / config/get / config/save) - rules 兼容对象与历史字符串两种形态;配置 id 用字符串承载避免雪花 ID 精度丢失 Co-Authored-By: Claude Code --- src/api/trade/pricing/index.ts | 35 ++ .../system/price/component/businessEditor.vue | 192 +++++++ .../system/price/component/modelEditor.vue | 479 ++++++++++++++++++ .../system/price/component/subjectList.vue | 281 ++++++++++ .../system/price/component/workflowEditor.vue | 334 ++++++++++++ src/views/system/price/index.vue | 449 ++++++++++++++++ src/views/system/price/mock.ts | 321 ++++++++++++ 7 files changed, 2091 insertions(+) create mode 100644 src/api/trade/pricing/index.ts create mode 100644 src/views/system/price/component/businessEditor.vue create mode 100644 src/views/system/price/component/modelEditor.vue create mode 100644 src/views/system/price/component/subjectList.vue create mode 100644 src/views/system/price/component/workflowEditor.vue create mode 100644 src/views/system/price/index.vue create mode 100644 src/views/system/price/mock.ts diff --git a/src/api/trade/pricing/index.ts b/src/api/trade/pricing/index.ts new file mode 100644 index 0000000..d0bd679 --- /dev/null +++ b/src/api/trade/pricing/index.ts @@ -0,0 +1,35 @@ +import request from '/@/utils/request'; + +/** 计价对象枚举 */ +export function getSubjects() { + return request({ + url: '/shop-user-trade/pricing/controller/subjects', + method: 'get', + }); +} + +/** 计价配置详情 */ +export function getPricingConfig(params: { subjectType: string; subjectId: string }) { + return request({ + url: '/shop-user-trade/pricing/controller/config/get', + method: 'get', + params, + }); +} + +/** 新增/更新计价配置(id > 0 更新,0 新增;rules 传费率对象;id 兼容雪花字符串避免精度丢失) */ +export function savePricingConfig(data: { + id: string | number; + subjectType: string; + subjectId: string; + rules: object; + minBalance?: number; + currency?: string; + enabled?: number; +}) { + return request({ + url: '/shop-user-trade/pricing/controller/config/save', + method: 'post', + data, + }); +} diff --git a/src/views/system/price/component/businessEditor.vue b/src/views/system/price/component/businessEditor.vue new file mode 100644 index 0000000..5305f28 --- /dev/null +++ b/src/views/system/price/component/businessEditor.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/src/views/system/price/component/modelEditor.vue b/src/views/system/price/component/modelEditor.vue new file mode 100644 index 0000000..2e91266 --- /dev/null +++ b/src/views/system/price/component/modelEditor.vue @@ -0,0 +1,479 @@ + + + + + diff --git a/src/views/system/price/component/subjectList.vue b/src/views/system/price/component/subjectList.vue new file mode 100644 index 0000000..55c50db --- /dev/null +++ b/src/views/system/price/component/subjectList.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/src/views/system/price/component/workflowEditor.vue b/src/views/system/price/component/workflowEditor.vue new file mode 100644 index 0000000..affe3da --- /dev/null +++ b/src/views/system/price/component/workflowEditor.vue @@ -0,0 +1,334 @@ + + + + + diff --git a/src/views/system/price/index.vue b/src/views/system/price/index.vue new file mode 100644 index 0000000..4f5fd4c --- /dev/null +++ b/src/views/system/price/index.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/src/views/system/price/mock.ts b/src/views/system/price/mock.ts new file mode 100644 index 0000000..b675e28 --- /dev/null +++ b/src/views/system/price/mock.ts @@ -0,0 +1,321 @@ +/** + * 价格管理(计价配置管理)—— 类型定义、接口映射与本地示例数据 + * + * 说明: + * - 计价对象枚举来自后端 GET .../subjects(见 src/api/trade/pricing/index.ts), + * 本文件提供后端项 → 页面 Subject 的映射(中文 modelType/modelTypeCode → 英文枚举等) + * - 计价配置读取后端的 rules JSON 字符串(JSON.parse),保存时 JSON.stringify 提交 + * - 模型类型编码:reason=100推理 / image=200图片 / audio=300音频 / video=600视频 + * - EXAMPLES 仅为编辑器本地"示例填充" UX,不参与后端数据 + */ + +/* ================= 枚举类型 ================= */ + +export type SubjectType = 'workflow' | 'model' | 'business'; + +export type ModelType = 'reason' | 'video' | 'audio' | 'image'; + +export type Unit = + | 'per_1M' + | 'per_1K' + | 'per_1' + | 'per_second' + | 'per_minute' + | 'per_hour' + | 'per_char'; + +export type Currency = 'CNY' | 'USD'; + +/* ================= 后端枚举接口 ================= */ + +/** 后端 GET .../subjects 返回的计价对象项 */ +export interface ApiSubjectItem { + subjectType: SubjectType; + subjectId: string; + name: string; + /** 仅 model:中文类型(推理/视频/音频/图片) */ + modelType?: string; + /** 仅 model:模型类型编码(100推理/200图片/300音频/600视频) */ + modelTypeCode?: number; +} + +/** modelTypeCode → 英文模型类型 */ +export const MODEL_TYPE_CODE_TO_EN: Record = { + 100: 'reason', + 200: 'image', + 300: 'audio', + 600: 'video', +}; + +/** 各 subjectType 固定的计费模式(后端不下发 modes,按类型硬编码展示) */ +export const SUBJECT_MODE_MAP: Partial> = { + workflow: ['per_item', 'per_second', 'per_token'], + business: ['per_period'], +}; + +/** 后端枚举项 → 页面 Subject */ +export function toSubject(item: ApiSubjectItem): Subject { + if (item.subjectType === 'model') { + const modelType = MODEL_TYPE_CODE_TO_EN[item.modelTypeCode ?? -1] ?? 'reason'; + return { + type: 'model', + id: item.subjectId, + name: item.name, + modelType, + modelTypeCode: item.modelTypeCode, + typeLabel: item.modelType || modelType, + hasConfig: false, + }; + } + return { + type: item.subjectType, + id: item.subjectId, + name: item.name, + modes: SUBJECT_MODE_MAP[item.subjectType] ?? [], + hasConfig: false, + }; +} + +/* ================= 计价对象枚举(页面态) ================= */ + +export interface Subject { + type: SubjectType; + /** subjectId */ + id: string; + /** 显示名称 */ + name: string; + /** 仅 model:模型类型(英文枚举,由 modelTypeCode 映射) */ + modelType?: ModelType; + /** 仅 model:模型类型编码 */ + modelTypeCode?: number; + /** 仅 model:模型中文类型(推理/视频/音频/图片) */ + typeLabel?: string; + /** workflow/business:支持的计费模式(本地硬编码) */ + modes?: string[]; + /** 是否已配置计价(选中后由 config/get 判断) */ + hasConfig: boolean; +} + +/* ================= 工作流费率 JSON ================= */ + +export interface WorkflowTier { + /** 档位最大秒数 */ + maxSec: number; + /** 档位价格(元) */ + price: number; +} + +export interface WorkflowRules { + /** 按条计费 */ + per_item?: { + tiers: WorkflowTier[]; + /** 超出最大档单价(元/秒) */ + overflowUnitPrice: number; + }; + /** 按秒计费 */ + per_second?: { + unitPrice: number; + }; + /** 按 token 计费(价格取自各模型,仅占位空对象) */ + per_token?: Record; +} + +/* ================= 模型费率 JSON ================= */ + +/** 编辑器内部规则行态(thinking/outputAudio 用字符串承载"未设置") */ +export interface ModelRule { + name: string; + mediaType: '' | 'text' | 'audio' | 'video' | 'image'; + thinking: '' | 'true' | 'false'; + outputAudio: '' | 'true' | 'false'; + outputResolution: string; + inLenMin?: number; + inLenMax?: number; + /** token 单位:输入单价 */ + input: number; + /** token 单位:输出单价 */ + output: number; + /** token 单位:缓存命中输入单价 */ + cacheHit: number; + /** 非 token 单位:每单位单价 */ + unitPrice: number; +} + +/** 示例数据源规则(thinking/outputAudio 为布尔,由 loadExample 转换) */ +export interface ExampleRule { + name: string; + mediaType?: string; + thinking?: boolean; + outputAudio?: boolean; + outputResolution?: string; + inLenMin?: number; + inLenMax?: number; + input?: number; + output?: number; + cacheHit?: number; + unitPrice?: number; +} + +/** 构建态:规则命中条件 */ +export interface BuiltModelRuleMatch { + mediaType?: string; + thinking?: boolean; + outputAudio?: boolean; + outputResolution?: string; + inputLengthMin?: number; + inputLengthMax?: number; +} + +/** 构建态:单条规则 */ +export interface BuiltModelRule { + name: string; + match?: BuiltModelRuleMatch; + price: Record; +} + +/** 模型费率 JSON 结构(对齐后端,含内嵌币种) */ +export interface ModelRules { + unit: Unit; + tiered: boolean; + rules: BuiltModelRule[]; + currency: Currency; +} + +/** 模型示例 */ +export interface ModelExample { + label: string; + unit: Unit; + tiered: boolean; + currency?: Currency; + minBalance?: number; + rules: ExampleRule[]; +} + +export type ModelExampleMap = Record; + +/* ================= 业务模块费率 JSON ================= */ + +export interface BusinessRules { + period: 'year'; + price: number; +} + +/* ================= 编辑器初始化配置 ================= */ + +/** 后端 config/get 解析后的编辑器初始化数据(公共字段 + 已 parse 的费率 JSON) */ +export interface EditorInitConfig { + /** 配置记录 id(>0 更新,0 新增;雪花 id 超安全整数,用字符串承载避免精度丢失) */ + id: string | number; + minBalance: number; + currency: Currency; + enabled: boolean; + /** 已 parse 的费率 JSON(编辑器按 subjectType 断言具体结构) */ + rules: WorkflowRules | ModelRules | BusinessRules; +} + +/* ================= 常量 ================= */ + +export const WORKFLOW_DEFAULT_TIERS: WorkflowTier[] = [ + { maxSec: 15, price: 29 }, + { maxSec: 30, price: 49 }, + { maxSec: 60, price: 89 }, +]; + +export const MODEL_TYPE_CODES: Record = { + reason: 100, + image: 200, + audio: 300, + video: 600, +}; + +export const UNIT_OPTIONS: { value: Unit; label: string }[] = [ + { value: 'per_1M', label: 'per_1M(每百万token)' }, + { value: 'per_1K', label: 'per_1K(每千token)' }, + { value: 'per_1', label: 'per_1(每个/每张图)' }, + { value: 'per_second', label: 'per_second(每秒)' }, + { value: 'per_minute', label: 'per_minute(每分钟)' }, + { value: 'per_hour', label: 'per_hour(每小时)' }, + { value: 'per_char', label: 'per_char(每字)' }, +]; + +export const MEDIA_TYPE_OPTIONS: { value: ModelRule['mediaType']; label: string }[] = [ + { value: '', label: '不限制' }, + { value: 'text', label: '文本 text' }, + { value: 'audio', label: '音频 audio' }, + { value: 'video', label: '视频 video' }, + { value: 'image', label: '图片 image' }, +]; + +export const BOOL_TRIPLE_OPTIONS: { value: '' | 'true' | 'false'; label: string }[] = [ + { value: '', label: '不限制' }, + { value: 'true', label: '是' }, + { value: 'false', label: '否' }, +]; + +/** 模型示例(本地填充 UX,逐字对应原型定价配置) */ +export const EXAMPLES: ModelExampleMap = { + video: [ + { + label: '视频·按秒(分辨率×有声/无声×输入媒体)', + unit: 'per_second', + tiered: false, + rules: [ + { name: '无声-720p-输入不含视频', mediaType: 'text', outputAudio: false, outputResolution: '720p', unitPrice: 0.8 }, + { name: '有声-1080p-输入含视频', mediaType: 'video', outputAudio: true, outputResolution: '1080p', unitPrice: 1.5 }, + ], + }, + { + label: '视频·按token(百万)', + unit: 'per_1M', + tiered: false, + rules: [ + { name: '文本输入-无声-1080p', mediaType: 'text', outputAudio: false, outputResolution: '1080p', input: 0.6, output: 3.6, cacheHit: 0.12 }, + { name: '视频输入-有声-1080p', mediaType: 'video', outputAudio: true, outputResolution: '1080p', input: 9, output: 27, cacheHit: 1.8 }, + ], + }, + ], + reason: [ + { + label: '推理·阶梯(思考×输入token档×输入媒体)', + unit: 'per_1M', + tiered: true, + rules: [ + { name: '思考-输入≤32000-文本', mediaType: 'text', thinking: true, inLenMax: 32000, input: 0.6, output: 3.6, cacheHit: 0.12 }, + { name: '思考-输入32001~128000-文本', mediaType: 'text', thinking: true, inLenMin: 32001, inLenMax: 128000, input: 0.9, output: 5.4, cacheHit: 0.18 }, + { name: '思考-输入含音频', mediaType: 'audio', thinking: true, input: 9, output: 3.6, cacheHit: 1.8 }, + { name: '非思考-统一价', thinking: false, input: 0.3, output: 1.2, cacheHit: 0.06 }, + ], + }, + ], + audio: [ + { + label: '音频·按字数', + unit: 'per_char', + tiered: false, + rules: [{ name: '语音-统一价(万字符5元)', unitPrice: 0.0005 }], + }, + { + label: '音频·按分钟', + unit: 'per_minute', + tiered: false, + rules: [{ name: '语音-统一价', unitPrice: 2 }], + }, + { + label: '音频·按token(百万)', + unit: 'per_1M', + tiered: false, + rules: [{ name: '统一价', input: 500, output: 500, cacheHit: 0 }], + }, + ], + image: [ + { + label: '图片·按张数×分辨率', + unit: 'per_1', + tiered: false, + rules: [ + { name: '512x512', outputResolution: '512x512', unitPrice: 0.2 }, + { name: '1024x1024', outputResolution: '1024x1024', unitPrice: 0.5 }, + ], + }, + ], +};