From 1026d909e94abc56dc6e761d2bfb18281e70bb43 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Mon, 5 Oct 2026 18:55:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AE=A2=E6=88=B7=20?= =?UTF-8?q?OpenAPI=20=E6=96=87=E6=A1=A3=E9=A1=B5=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=BD=A8=E8=BF=B9=E4=B8=8E=E6=96=87=E4=BB=B6=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 菜单走 SPA 回退;附件增加 POD;调试页去掉接口调试 tab。 Co-authored-by: Cursor --- nginx-default.conf | 2 +- nginx-docker.conf | 2 +- nginx-k3s.conf | 2 +- src/constants/pageButtonCatalog.ts | 7 + src/locale/en-US.ts | 139 ++- src/locale/zh-CN.ts | 139 ++- src/router/routes/newModules/system.ts | 12 + src/utils/menu-i18n.ts | 1 + src/views/api/apiDebugger/index.vue | 45 +- src/views/api/openApi/FieldTable.vue | 67 ++ src/views/api/openApi/OpenApiDocs.vue | 959 ++++++++++++++++++ src/views/api/openApi/catalog.ts | 329 ++++++ src/views/api/openApi/index.vue | 42 + src/views/api/openApi/sign.ts | 61 ++ .../order/components/OrderAttachmentsTab.vue | 6 + vite.config.ts | 4 +- 16 files changed, 1762 insertions(+), 55 deletions(-) create mode 100644 src/views/api/openApi/FieldTable.vue create mode 100644 src/views/api/openApi/OpenApiDocs.vue create mode 100644 src/views/api/openApi/catalog.ts create mode 100644 src/views/api/openApi/index.vue create mode 100644 src/views/api/openApi/sign.ts diff --git a/nginx-default.conf b/nginx-default.conf index b4f2c6d..7c38156 100644 --- a/nginx-default.conf +++ b/nginx-default.conf @@ -106,7 +106,7 @@ server { } # ============ API 前端路由(必须先于 /api/ 代理)============ - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) { + location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { try_files /index.html =200; } diff --git a/nginx-docker.conf b/nginx-docker.conf index 075a684..a9c925d 100644 --- a/nginx-docker.conf +++ b/nginx-docker.conf @@ -69,7 +69,7 @@ server { try_files /index.html =200; } - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) { + location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { try_files /index.html =200; } diff --git a/nginx-k3s.conf b/nginx-k3s.conf index b4faf0d..14775c0 100644 --- a/nginx-k3s.conf +++ b/nginx-k3s.conf @@ -70,7 +70,7 @@ server { try_files $uri @spa; } - location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest)(/|$) { + location ~ ^/api/(upstreamInterface|upstreamInterfaceConfig|upstreamInterfaceConfigEdit|downstreamInterface(|/newPort)|appList(|/createApplication)|requestLog|callStatistics|apiDebugger|apiTest|openApi)(/|$) { try_files $uri @spa; } diff --git a/src/constants/pageButtonCatalog.ts b/src/constants/pageButtonCatalog.ts index 889281d..43a9f77 100644 --- a/src/constants/pageButtonCatalog.ts +++ b/src/constants/pageButtonCatalog.ts @@ -279,6 +279,7 @@ export const PAGE_BUTTON_PARENT_ALIASES: Record = { 'System.BatchEmail': 'System.BatchEmail', 'Api.ApiTest': 'Api.ApiTest', 'Api.Test.Query': 'Api.ApiTest', + 'Api.OpenApi.Query': 'Api.OpenApi', 'Channel.LabelDesign': 'Channel.LabelDesign', 'Order.BatchJobs.Query': 'Order.BatchJobs', 'Order.WaybillManage.Query': 'Order.WaybillManage', @@ -420,6 +421,12 @@ const CORE_GROUPS: PageButtonGroup[] = [ { code: 'Copy', name: '复制' }, { code: 'Format', name: '格式化' }, ]), + B('Api.OpenApi', [ + { code: 'Api.OpenApi.Query', name: '查询' }, + { code: 'SendDebug', name: '发送调试' }, + { code: 'ImportCurl', name: '导入cURL' }, + { code: 'Copy', name: '复制' }, + ]), // ── 轨迹 ── B('Track.Order', [ diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 2cf0d2c..7fdf541 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -1112,6 +1112,121 @@ export default { 'menu.api.apiDebugger': 'API Debugger', 'menu.api.apiLike': 'API LIKE', 'menu.api.apiTest': 'API Test', + 'menu.api.openApi': 'OpenAPI', + 'api.openApi.tab.docs': 'Reference', + 'api.openApi.tab.debug': 'Try it', + 'api.openApi.navTitle': 'On this page', + 'api.openApi.nav.overview': 'Overview', + 'api.openApi.nav.auth': 'Authentication', + 'api.openApi.nav.errors': 'Errors', + 'api.openApi.nav.parcel': 'Create parcel', + 'api.openApi.nav.pickup': 'Pickup push fee', + 'api.openApi.nav.track': 'Query track', + 'api.openApi.nav.files': 'Get files', + 'api.openApi.title': 'OpenAPI Reference', + 'api.openApi.lead': + 'Machine-to-machine APIs for customer systems. Read authentication first, then call resources. Code samples sit on the right, Stripe / Twilio style.', + 'api.openApi.baseUrl': 'Base URL', + 'api.openApi.contentType': 'Content-Type', + 'api.openApi.copy': 'Copy', + 'api.openApi.copyCurl': 'Copy cURL', + 'api.openApi.copySuccess': 'Copied', + 'api.openApi.copyFailed': 'Copy failed', + 'api.openApi.tryIt': 'Try in debugger', + 'api.openApi.overviewBody': + 'OpenAPI calls go through the Admin gateway under /openapi. JSON fields are camelCase. Customer order APIs do not use login JWT; they use AppKey plus HMAC.', + 'api.openApi.overviewPoint1': + 'Credentials: generate AppKey / AppSecret in customer management. The full secret is shown only on create or reset — store it offline.', + 'api.openApi.overviewPoint2': + 'Signing: SHA-256 the raw HTTP body bytes, build stringToSign with appKey and timestamp, then HMAC-SHA256 with AppSecret. The secret is never sent.', + 'api.openApi.overviewPoint3': + 'Clock: timestamp is Unix seconds. Drift beyond 300 seconds against server UTC returns “request expired”. Use NTP.', + 'api.openApi.overviewPoint4': + 'All calls are POST + JSON. Treat the envelope code (string) as the business status; HTTP is usually 200. Do not send a login JWT.', + 'api.openApi.overviewPoint5': + 'clientNo is the create-parcel idempotency key: the same customer resubmitting the same number gets the original order (code=200), not a new one.', + 'api.openApi.compatTitle': 'Compatibility', + 'api.openApi.compatBody': + 'We only add fields. Existing field meanings and success code values stay stable. Ignore unknown fields. Read by name, not by order.', + 'api.openApi.col.method': 'Method', + 'api.openApi.col.path': 'Path', + 'api.openApi.col.auth': 'Auth', + 'api.openApi.authHmac': 'Header HMAC (appKey / timestamp / sign)', + 'api.openApi.authUpstream': 'Not customer HMAC; authenticated by itfx then forwarded', + 'api.openApi.signedEndpointNote': + 'Required headers: appKey, timestamp, sign. Sign the raw body bytes of this request.', + 'api.openApi.trackLookupNote': + 'If several numbers are sent, clientNo wins. The order must belong to the credential customer, or you get “order not found”. Tracking is queried by service number; missing serviceNo fails.', + 'api.openApi.signPitfall1': + 'UTF-8 without BOM. The separators in stringToSign are LF (\\n), not Windows \\r\\n.', + 'api.openApi.signPitfall2': + 'Header names are appKey, timestamp, sign (not Authorization). timestamp is Unix seconds, not milliseconds.', + 'api.openApi.signPitfall3': + 'Pretty-print, key reorder, or HttpClient rewriting Content changes bytes and yields invalid signature. Sign the exact string you send.', + 'api.openApi.errMsgTitle': 'Common msg values (match the string)', + 'api.openApi.msgInvalidCred': + 'Missing headers, unknown/disabled AppKey, or abnormal customer. Same wording on purpose.', + 'api.openApi.msgInvalidSign': 'HMAC mismatch: body bytes differ from what was signed, or wrong Secret.', + 'api.openApi.msgExpired': 'timestamp is not an integer, or skew exceeds ±300 seconds.', + 'api.openApi.msgBody': 'Empty body or JSON that cannot be deserialized.', + 'api.openApi.msgOrderMissing': 'Number not found, or the order is not owned by this AppKey’s customer.', + 'api.openApi.msgLookup': 'Track/files need at least one locator field.', + 'api.openApi.msgFileType': 'Unknown type, or a type customers cannot query (e.g. downstreamLabel).', + 'api.openApi.authTitle': 'Authentication', + 'api.openApi.authIntro': + 'Applies to create parcel, query track, and get files. A three-header scheme similar to AWS Signature / Alipay Open Platform: identity in headers, secret used only locally.', + 'api.openApi.headersTitle': 'Headers', + 'api.openApi.col.header': 'Header', + 'api.openApi.col.field': 'Field', + 'api.openApi.col.type': 'Type', + 'api.openApi.col.required': 'Required', + 'api.openApi.col.desc': 'Description', + 'api.openApi.col.code': 'code', + 'api.openApi.col.msg': 'msg', + 'api.openApi.yes': 'Yes', + 'api.openApi.no': 'No', + 'api.openApi.headerAppKey': 'Customer OpenAPI AppKey', + 'api.openApi.headerTimestamp': 'Unix seconds (UTC). ±300s skew allowed', + 'api.openApi.headerSign': 'Lowercase hex. HMAC-SHA256(appSecret, stringToSign)', + 'api.openApi.signAlgoTitle': 'Signature algorithm', + 'api.openApi.signStep1': 'Take the exact HTTP body string you will send (UTF-8). Do not minify then re-space.', + 'api.openApi.signStep2': 'bodyHash = lowercase hex of SHA-256(rawBody). Empty body hashes the empty string.', + 'api.openApi.signStep3': + 'stringToSign = appKey + LF + timestamp + LF + bodyHash (LF is \\n). sign = lowercase hex HMAC-SHA256(appSecret, stringToSign).', + 'api.openApi.signWarn': + 'Sign the body that is actually sent. Changing indentation, key order, or encoding after JSON.stringify invalidates the signature.', + 'api.openApi.playgroundTitle': 'Signature playground', + 'api.openApi.playgroundTip': + 'AppSecret is used only in the browser. Copy the computed sign into the cURL sample on the right.', + 'api.openApi.appKey': 'AppKey', + 'api.openApi.appSecret': 'AppSecret', + 'api.openApi.timestamp': 'timestamp', + 'api.openApi.rawBody': 'rawBody (signed as-is)', + 'api.openApi.appKeyPh': 'Customer AppKey', + 'api.openApi.appSecretPh': 'Local only', + 'api.openApi.refreshTs': 'Refresh', + 'api.openApi.errorsTitle': 'Errors', + 'api.openApi.errorsIntro': + 'Business results live in the JSON envelope; HTTP status is usually still 200. Read code first, then msg / data.', + 'api.openApi.err200': 'Success; data is the business object', + 'api.openApi.err400': + 'Auth or input failure: invalid credential, invalid signature, expired request, empty body, malformed JSON, etc.', + 'api.openApi.err500': 'Unclassified server error', + 'api.openApi.parcelSummary': + 'Create a self-ship parcel. Customer Id comes from the signed credential. clientNo is required and is the idempotency key: the same customer resubmitting the same clientNo gets the existing order. inboundMethod is always selfShip.', + 'api.openApi.requestBody': 'Request Body', + 'api.openApi.responseBody': 'Response data', + 'api.openApi.envelopeHint': 'Envelope is { code, msg, data }. Table below is data.', + 'api.openApi.pickupSummary': + 'Push pickup weight/size and compute receivable/payable for the current product. For carriers / itfx upstream.', + 'api.openApi.pickupAuthNote': + 'This endpoint does not check customer AppKey. Authentication happens on the itfx upstream before OMS.', + 'api.openApi.trackSummary': + 'Query tracking for the authenticated customer. Provide at least one of clientNo / serviceNo / trackNo. Events are looked up in the track center by service number. events is [] when none exist.', + 'api.openApi.filesSummary': + 'Return download URLs for one attachment type on that order. Only orders owned by the credential customer. Downstream labels are not available.', + 'api.openApi.fileTypeEnum': 'fileType values', + 'api.openApi.sampleHint': 'Replace keys and numbers with your environment. Request / Response are JSON; cURL and SDKs follow the current endpoint.', // API - Call Statistics 'api.callStatistics.name.app': 'Application Name', @@ -2009,6 +2124,7 @@ export default { 'order.myOrders.drawer.attachments.types.downstreamLabel': 'Downstream label', 'order.myOrders.drawer.attachments.types.lastMileLabel': 'Last-mile label', 'order.myOrders.drawer.attachments.types.infraredImage': 'Infrared image', + 'order.myOrders.drawer.attachments.types.pod': 'POD', 'order.myOrders.drawer.attachments.source.upload': 'Upload', 'order.myOrders.drawer.attachments.source.systemGenerate': 'System generated', 'order.myOrders.drawer.attachments.source.downstream': 'Downstream', @@ -2355,6 +2471,7 @@ export default { 'order.priceCheck.table.askAiCanShip': 'Can this ship?', 'order.priceCheck.table.askAiCheaper': 'Anything cheaper?', 'order.priceCheck.table.empty': 'No matching quotes', + 'order.priceCheck.table.emptyWithFailures': '{count} misses — see reasons on the right', 'order.priceCheck.table.emptyHint': 'Try adding postal code, dimensions, or cargo/warehouse filters; contact support if needed.', 'order.priceCheck.tag.priceOptimal': 'Best price', @@ -2398,6 +2515,17 @@ export default { 'order.priceCheck.tradeTerms.ddp': 'DDP (duties included)', 'order.priceCheck.tradeTerms.ddu': 'DDU (duties excluded)', 'order.priceCheck.guide.failureTitle': 'No quotes found', + 'order.priceCheck.guide.failureSummary': '{productCount} products, {itemCount} misses, sorted by product', + 'order.priceCheck.guide.failureUnknownProduct': 'Unspecified product', + 'order.priceCheck.guide.failureCargo': 'Cargo: {name}', + 'order.priceCheck.guide.failureWarehouse': 'Warehouse: {name}', + 'order.priceCheck.guide.failureStepLabel': 'Step: {step}', + 'order.priceCheck.guide.failureStep.product': 'Product', + 'order.priceCheck.guide.failureStep.snapshot': 'Price snapshot', + 'order.priceCheck.guide.failureStep.blacklist': 'Blacklist', + 'order.priceCheck.guide.failureStep.weight': 'Weight', + 'order.priceCheck.guide.failureStep.zone': 'Price zone', + 'order.priceCheck.guide.failureStep.price': 'Sell price', 'order.priceCheck.ai.title': 'AI Route Pick', 'order.priceCheck.ai.desc': 'Enter cargo name — we check customer guides for can-ship and best route', 'order.priceCheck.ai.descShort': 'Pick a route by cargo name', @@ -5353,6 +5481,7 @@ export default { 'channel.area.matchMode.prefix': 'Prefix', 'channel.area.matchMode.exact': 'Exact', 'channel.area.matchMode.range': 'Range', + 'channel.area.matchMode.all': 'Any', 'channel.area.form.placeholder.keyword': 'Enter keyword', 'channel.area.form.placeholder.areaName': 'Enter area name', 'channel.area.form.placeholder.input': 'Please enter', @@ -5517,8 +5646,8 @@ export default { 'channel.weightRounding.section.basic': 'Basic info', 'channel.weightRounding.section.rule': 'Rounding range', 'channel.weightRounding.tip.roundingName': 'Rounding scheme name shown in the left list', - 'channel.weightRounding.tip.startValue': 'Inclusive lower bound; must be less than end value', - 'channel.weightRounding.tip.endValue': 'Upper bound; must be greater than start value', + 'channel.weightRounding.tip.startValue': 'Exclusive lower bound (left-open); must be less than end value. Sharing the next range’s start is allowed', + 'channel.weightRounding.tip.endValue': 'Inclusive upper bound; must be greater than start value. Interval is (start, end]', 'channel.weightRounding.tip.public': 'Round-up step in kg, e.g. 0.5 rounds up per 0.5 kg', 'channel.weightRounding.modal.addRounding': 'Add rounding', 'channel.weightRounding.modal.editRounding': 'Edit rounding', @@ -5541,9 +5670,11 @@ export default { 'channel.weightRounding.tip.emptySelectDesc': 'Select or create a rounding scheme on the left, then manage weight intervals on the right.', 'channel.weightRounding.tip.addRoundingGroup': 'Rounding schemes define how billed weight is rounded, e.g. 0.5 kg or 1 kg steps.', 'channel.weightRounding.tip.editRoundingGroup': 'Renaming a scheme does not change existing interval rules.', - 'channel.weightRounding.tip.addRule': 'Start value must be less than end value. Rounding value is the step size (e.g. 0.5 rounds up per 0.5 kg).', - 'channel.weightRounding.tip.editRule': 'Changes affect rounding results. Ensure intervals do not overlap and values are correct.', + 'channel.weightRounding.tip.addRule': 'Ranges are left-open right-closed (start, end]. 0–10 and 10–25 must touch; 10 kg uses the first range. Rounding value is the step size (e.g. 0.5 rounds up per 0.5 kg).', + 'channel.weightRounding.tip.editRule': 'Changes affect rounding. Use (start, end]; adjacent ranges must touch. Crossing or leaving a gap is not allowed.', 'channel.weightRounding.form.rules.rangeInvalid': 'Start value must be less than end value', + 'channel.weightRounding.form.rules.rangeOverlap': 'Ranges cannot overlap: ({current}] crosses existing ({existed}]', + 'channel.weightRounding.form.rules.rangeGap': 'Ranges must touch end-to-start: gap between {left} and {right}', 'channel.weightRounding.form.rules.publicPositive': 'Rounding value must be a number greater than 0', 'channel.weightRounding.form.rules.numberRequired': 'Please enter {field}', 'channel.weightRounding.form.rules.numberInvalid': '{field} must be a valid number', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index b09f722..7ca8ef2 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -1104,6 +1104,121 @@ export default { 'menu.api.apiDebugger': 'API调试', 'menu.api.apiLike': 'API LIKE', 'menu.api.apiTest': 'API测试', + 'menu.api.openApi': 'OpenAPI', + 'api.openApi.tab.docs': '接口文档', + 'api.openApi.tab.debug': '接口调试', + 'api.openApi.navTitle': '目录', + 'api.openApi.nav.overview': '概述', + 'api.openApi.nav.auth': '鉴权与签名', + 'api.openApi.nav.errors': '错误与响应', + 'api.openApi.nav.parcel': '创建包裹', + 'api.openApi.nav.pickup': '揽收推费', + 'api.openApi.nav.track': '查询轨迹', + 'api.openApi.nav.files': '获取文件', + 'api.openApi.title': 'OpenAPI 参考', + 'api.openApi.lead': + '面向客户系统的机机接口。风格对齐 Stripe / Twilio:先读鉴权,再按资源调用;右侧为可复制的语言示例。', + 'api.openApi.baseUrl': 'Base URL', + 'api.openApi.contentType': 'Content-Type', + 'api.openApi.copy': '复制', + 'api.openApi.copyCurl': '复制 cURL', + 'api.openApi.copySuccess': '已复制', + 'api.openApi.copyFailed': '复制失败', + 'api.openApi.tryIt': '在调试器中试用', + 'api.openApi.overviewBody': + '所有 OpenAPI 均走 Admin 网关,路径以 /openapi 开头。JSON 字段为 camelCase。客户下单类接口不使用登录 JWT,而是用 AppKey + HMAC 签名。', + 'api.openApi.overviewPoint1': + '凭证:客户管理中生成 AppKey / AppSecret;Secret 只在生成或重置时完整展示一次,请离线保存。', + 'api.openApi.overviewPoint2': + '签名:对「原始 HTTP Body 字节」做 SHA-256,再与 appKey、timestamp 组成 stringToSign,用 AppSecret 做 HMAC-SHA256。Secret 永不出现在请求里。', + 'api.openApi.overviewPoint3': + '时钟:timestamp 为 Unix 秒,与服务器 UTC 相差超过 300 秒会返回「请求已过期」。请用 NTP 校时。', + 'api.openApi.overviewPoint4': + '全部为 POST + JSON。业务成败看信封 code(字符串),HTTP 多为 200。不要用登录 JWT。', + 'api.openApi.overviewPoint5': + 'clientNo 是创建包裹的幂等键:同一客户再次提交相同单号,返回原单(code=200),不会新建。', + 'api.openApi.compatTitle': '兼容约定', + 'api.openApi.compatBody': + '本接口只增加字段,不修改已有字段含义,也不把成功的 code 改成别的值。新增字段请忽略;客户端应按字段名取值,不要依赖字段顺序。', + 'api.openApi.col.method': 'Method', + 'api.openApi.col.path': 'Path', + 'api.openApi.col.auth': '鉴权', + 'api.openApi.authHmac': 'Header HMAC(appKey / timestamp / sign)', + 'api.openApi.authUpstream': '非客户 HMAC;由 itfx 上游鉴权后转发', + 'api.openApi.signedEndpointNote': + 'Header 必填 appKey、timestamp、sign。sign 对「本请求即将发出的 Body 原始字节」计算。', + 'api.openApi.trackLookupNote': + '同时传多个单号时优先 clientNo。查到的订单必须属于当前凭证客户,否则「订单不存在」。轨迹中心按服务单号查询,无服务单号会失败。', + 'api.openApi.signPitfall1': + 'UTF-8 无 BOM;stringToSign 中间是 LF(\\n),不是 Windows 的 \\r\\n。', + 'api.openApi.signPitfall2': + 'Header 名固定为 appKey、timestamp、sign(不要用 Authorization)。timestamp 是十进制 Unix 秒,不是毫秒。', + 'api.openApi.signPitfall3': + '美化 JSON、调字段顺序、HttpClient 自动改 Content 都会改字节导致「签名无效」。先签再发,发的就是签的那串。', + 'api.openApi.errMsgTitle': '常见 msg(请按字符串匹配)', + 'api.openApi.msgInvalidCred': + '缺 Header、AppKey 不存在/停用、客户状态异常。统一文案,不要依赖细分。', + 'api.openApi.msgInvalidSign': 'HMAC 对不上:Body 字节与签名时不一致,或 Secret 错误。', + 'api.openApi.msgExpired': 'timestamp 无法解析,或与服务器相差超过 ±300 秒。', + 'api.openApi.msgBody': '空 Body 或 JSON 无法反序列化。', + 'api.openApi.msgOrderMissing': '单号不存在,或不属于当前 AppKey 对应客户。', + 'api.openApi.msgLookup': '轨迹/文件查询至少要有一个定位字段。', + 'api.openApi.msgFileType': '未知类型,或客户不可查询(如 downstreamLabel)。', + 'api.openApi.authTitle': '鉴权与签名', + 'api.openApi.authIntro': + '适用接口:创建包裹、查询轨迹、获取文件。请求头三元组与 AWS Signature / 支付宝开放平台同类:身份在 Header,密钥只参与本地计算。', + 'api.openApi.headersTitle': '请求头', + 'api.openApi.col.header': 'Header', + 'api.openApi.col.field': '字段', + 'api.openApi.col.type': '类型', + 'api.openApi.col.required': '必填', + 'api.openApi.col.desc': '说明', + 'api.openApi.col.code': 'code', + 'api.openApi.col.msg': 'msg', + 'api.openApi.yes': '是', + 'api.openApi.no': '否', + 'api.openApi.headerAppKey': '客户开放接口 AppKey', + 'api.openApi.headerTimestamp': 'Unix 秒(UTC)。允许 ±300 秒时钟偏差', + 'api.openApi.headerSign': '小写 hex。HMAC-SHA256(appSecret, stringToSign)', + 'api.openApi.signAlgoTitle': '签名算法', + 'api.openApi.signStep1': '取将要发送的 HTTP Body 原始字符串 rawBody(UTF-8)。不要先 minify 再改空格。', + 'api.openApi.signStep2': 'bodyHash = SHA-256(rawBody) 的小写十六进制。空 Body 对空字符串哈希。', + 'api.openApi.signStep3': + 'stringToSign = appKey + LF + timestamp + LF + bodyHash(LF 即 \\n)。sign = HMAC-SHA256(appSecret, stringToSign) 小写 hex。', + 'api.openApi.signWarn': + '必须对「最终发出的 Body」签名。JSON.stringify 后再改缩进、字段顺序或编码,都会导致签名无效。', + 'api.openApi.playgroundTitle': '签名计算器', + 'api.openApi.playgroundTip': + 'AppSecret 只在浏览器本地计算,不会上传。算出的 sign 可复制到右侧 cURL。', + 'api.openApi.appKey': 'AppKey', + 'api.openApi.appSecret': 'AppSecret', + 'api.openApi.timestamp': 'timestamp', + 'api.openApi.rawBody': 'rawBody(参与签名)', + 'api.openApi.appKeyPh': '客户 AppKey', + 'api.openApi.appSecretPh': '仅本地计算', + 'api.openApi.refreshTs': '刷新', + 'api.openApi.errorsTitle': '错误与响应', + 'api.openApi.errorsIntro': + '业务结果放在 JSON 信封里,HTTP 状态通常仍为 200。请先读 code,再读 msg / data。', + 'api.openApi.err200': '调用成功,data 为业务对象', + 'api.openApi.err400': + '鉴权或参数失败:凭证无效、签名无效、请求已过期、请求体不能为空、请求体格式错误等', + 'api.openApi.err500': '未分类系统异常', + 'api.openApi.parcelSummary': + '创建自发货订单包裹。客户 Id 由签名凭证解析。clientNo 必填且为幂等键:相同客户重复提交同一 clientNo 时返回已存在订单。开放接口固定 inboundMethod = selfShip。', + 'api.openApi.requestBody': 'Request Body', + 'api.openApi.responseBody': 'Response data', + 'api.openApi.envelopeHint': '外层为 { code, msg, data },下表为 data 字段。', + 'api.openApi.pickupSummary': + '推送揽收件重尺并按当前产品计算应收应付。供承运 / itfx 上游转发。', + 'api.openApi.pickupAuthNote': + '本接口不校验客户 AppKey。鉴权在 itfx 上游完成后再转发到 OMS。', + 'api.openApi.trackSummary': + '查询本客户订单轨迹。clientNo / serviceNo / trackNo 至少填一项;轨迹按服务单号向轨迹中心查询。无节点时 events 为空数组。', + 'api.openApi.filesSummary': + '按附件类型返回该订单文件下载地址。仅能查询当前凭证所属客户的订单。不提供下游面单。', + 'api.openApi.fileTypeEnum': 'fileType 枚举', + 'api.openApi.sampleHint': '示例中的密钥与单号请换成你的环境数据。Request / Response 为 JSON;cURL 与 SDK 会跟当前接口切换。', // API - 调用统计 'api.callStatistics.name.app': '应用名称', @@ -1895,6 +2010,7 @@ export default { 'order.myOrders.drawer.attachments.types.downstreamLabel': '下游面单', 'order.myOrders.drawer.attachments.types.lastMileLabel': '末端面单', 'order.myOrders.drawer.attachments.types.infraredImage': '红外图片', + 'order.myOrders.drawer.attachments.types.pod': 'POD', 'order.myOrders.drawer.attachments.source.upload': '上传', 'order.myOrders.drawer.attachments.source.systemGenerate': '系统生成', 'order.myOrders.drawer.attachments.source.downstream': '下游回传', @@ -2241,6 +2357,7 @@ export default { 'order.priceCheck.table.askAiCanShip': '这个能不能发?', 'order.priceCheck.table.askAiCheaper': '有没有更便宜?', 'order.priceCheck.table.empty': '暂无匹配报价', + 'order.priceCheck.table.emptyWithFailures': '共 {count} 条未命中,详见右侧原因', 'order.priceCheck.table.emptyHint': '可尝试填写邮编、尺寸或选择货物属性/仓库后重试;也可联系客服协助查价。', 'order.priceCheck.tag.priceOptimal': '价格最优', @@ -2285,6 +2402,17 @@ export default { 'order.priceCheck.guide.close': '关闭说明', 'order.priceCheck.guide.idle': '点选左侧报价卡,查看对客说明与限制', 'order.priceCheck.guide.failureTitle': '查价未出结果', + 'order.priceCheck.guide.failureSummary': '共 {productCount} 个产品、{itemCount} 条未命中,按产品排序', + 'order.priceCheck.guide.failureUnknownProduct': '未指定产品', + 'order.priceCheck.guide.failureCargo': '货物:{name}', + 'order.priceCheck.guide.failureWarehouse': '仓库:{name}', + 'order.priceCheck.guide.failureStepLabel': '步骤:{step}', + 'order.priceCheck.guide.failureStep.product': '产品', + 'order.priceCheck.guide.failureStep.snapshot': '价格快照', + 'order.priceCheck.guide.failureStep.blacklist': '黑名单', + 'order.priceCheck.guide.failureStep.weight': '计重', + 'order.priceCheck.guide.failureStep.zone': '价格分区', + 'order.priceCheck.guide.failureStep.price': '卖价', 'order.priceCheck.ai.title': 'AI 荐线', 'order.priceCheck.ai.desc': '输入品名,对照对客说明判断能否寄、哪条更合适', 'order.priceCheck.ai.descShort': '输入品名,对照对客说明荐线', @@ -5390,6 +5518,7 @@ export default { 'channel.area.matchMode.prefix': '前缀', 'channel.area.matchMode.exact': '精确', 'channel.area.matchMode.range': '区间', + 'channel.area.matchMode.all': '不限', 'channel.area.form.placeholder.keyword': '请输入关键字', 'channel.area.form.placeholder.areaName': '请输入分区名称', 'channel.area.form.placeholder.input': '请输入', @@ -5569,8 +5698,8 @@ export default { 'channel.weightRounding.section.basic': '基础信息', 'channel.weightRounding.section.rule': '进位区间', 'channel.weightRounding.tip.roundingName': '进位制名称,用于左侧列表区分不同取整方案', - 'channel.weightRounding.tip.startValue': '重量区间下限(含),须小于截止值', - 'channel.weightRounding.tip.endValue': '重量区间上限,须大于起始值', + 'channel.weightRounding.tip.startValue': '重量区间下限(不含),须小于截止值;与下一段起始值相同不算重叠', + 'channel.weightRounding.tip.endValue': '重量区间上限(含),须大于起始值;左开右闭 (起始, 截止]', 'channel.weightRounding.tip.public': '向上取整步长(kg),例如 0.5 表示按 0.5kg 进位', 'channel.weightRounding.modal.addRounding': '新增进位制', 'channel.weightRounding.modal.editRounding': '修改进位制', @@ -5593,9 +5722,11 @@ export default { 'channel.weightRounding.tip.emptySelectDesc': '在左侧选择或新建进位制,选中后可在右侧维护各重量区间的进位规则。', 'channel.weightRounding.tip.addRoundingGroup': '进位制用于定义计费重量的取整方式,例如「0.5kg 进位」或「1kg 进位」。', 'channel.weightRounding.tip.editRoundingGroup': '修改进位制名称不会影响已有区间配置。', - 'channel.weightRounding.tip.addRule': '起始值须小于截止值;进位值表示向上取整的步长,例如 0.5 表示按 0.5kg 向上进位。', - 'channel.weightRounding.tip.editRule': '修改后将影响计费重量进位结果,请确认区间不重叠且数值准确。', + 'channel.weightRounding.tip.addRule': '区间为左开右闭 (起始, 截止]:例如 0~10 与 10~25 必须首尾相接,10kg 走前一段。进位值表示向上取整步长,例如 0.5 表示按 0.5kg 向上进位。', + 'channel.weightRounding.tip.editRule': '修改后将影响计费重量进位结果。请按左开右闭配置,相邻区间必须端点相接,不能交叉或留空档。', 'channel.weightRounding.form.rules.rangeInvalid': '起始值须小于截止值', + 'channel.weightRounding.form.rules.rangeOverlap': '起始区间不能重叠:({current}] 与已有 ({existed}] 交叉', + 'channel.weightRounding.form.rules.rangeGap': '起始区间必须首尾相接:{left} 与 {right} 之间有缺口', 'channel.weightRounding.form.rules.publicPositive': '进位值须为大于 0 的数字', 'channel.weightRounding.form.rules.numberRequired': '请填写{field}', 'channel.weightRounding.form.rules.numberInvalid': '{field}须为有效数字', diff --git a/src/router/routes/newModules/system.ts b/src/router/routes/newModules/system.ts index 4bfb152..2cc6167 100644 --- a/src/router/routes/newModules/system.ts +++ b/src/router/routes/newModules/system.ts @@ -126,6 +126,18 @@ const SYSTEM: AppRouteRecordRaw = { }, }, + { + path: 'openApi', + name: 'OpenApiDocs', + component: () => import('@/views/api/openApi/index.vue'), + meta: { + locale: 'menu.api.openApi', + requiresAuth: true, + permission: 'Api.OpenApi.Query', + order: 8, + icon: 'icon-book', + }, + }, { path: 'apiDebugger', name: 'ApiDebugger', diff --git a/src/utils/menu-i18n.ts b/src/utils/menu-i18n.ts index 1ea2d8d..86025df 100644 --- a/src/utils/menu-i18n.ts +++ b/src/utils/menu-i18n.ts @@ -75,6 +75,7 @@ export const MENU_NAME_TO_LOCALE_KEY: Record = { API调试: 'menu.api.apiDebugger', 'API LIKE': 'menu.api.apiLike', API测试: 'menu.api.apiTest', + OpenAPI: 'menu.api.openApi', 系统: 'menu.system', 组织管理: 'menu.system.organization', 用户管理: 'menu.system.user', diff --git a/src/views/api/apiDebugger/index.vue b/src/views/api/apiDebugger/index.vue index 7067c58..8b8647d 100644 --- a/src/views/api/apiDebugger/index.vue +++ b/src/views/api/apiDebugger/index.vue @@ -1,33 +1,17 @@