Compare commits

...

2 Commits

Author SHA1 Message Date
272d97488d Merge remote-tracking branch 'origin/master' 2026-06-01 11:25:26 +08:00
0e7b5e0ae1 OCR功能通过 2026-06-01 11:24:20 +08:00
20 changed files with 307 additions and 115 deletions

View File

@ -60,14 +60,9 @@ ALIYUN_AI_REGION=cn-hangzhou
ALIYUN_AI_ACCESS_KEY_ID=xxx
# 阿里云 AI 服务的 AccessKey Secret
ALIYUN_AI_ACCESS_KEY_SECRET=xxx
# OCR 模型名称(使用默认模型时填 default
ALIYUN_OCR_MODEL=default
# OCR 服务自定义端点地址(留空则使用默认端点)
ALIYUN_OCR_ENDPOINT=
# OCR 服务请求路径(留空则使用默认路径)
ALIYUN_OCR_PATH=
# OCR 应用码(部分 OCR 服务需要此配置)
ALIYUN_OCR_APPCODE=
# LLM 解析 API KeyDashScope 通义千问)
LLM_PARSE_API_KEY=
# ==================== 物流轨迹查询 ====================

View File

@ -9,7 +9,10 @@ URL 前缀:/api/files
权限要求已登录用户get_current_user
"""
from fastapi import APIRouter, Depends
from datetime import datetime
from pathlib import Path
from fastapi import APIRouter, Depends, UploadFile
from sqlalchemy.orm import Session
from backend.app.api.deps import get_current_user
@ -62,3 +65,25 @@ def save_attachment(
权限要求已登录用户
"""
return success_payload(file_service.save_attachment({**payload.model_dump(), "created_by": current_user.get("user_id")}, session))
LOCAL_UPLOAD_DIR = Path("D:/tmp/order-flow-oss/local-uploads")
@router.post("/local-upload")
async def local_upload(
file: UploadFile,
current_user: dict = Depends(get_current_user),
) -> dict:
"""开发环境本地文件上传,保存到本地目录并返回访问 URL。
绕过 OSS 直传适用于本地开发调试场景
"""
LOCAL_UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
safe_name = Path(file.filename or "upload").name
file_path = LOCAL_UPLOAD_DIR / f"{timestamp}_{safe_name}"
content = await file.read()
file_path.write_bytes(content)
url = f"http://127.0.0.1:8000/uploads/{timestamp}_{safe_name}"
return success_payload({"url": url, "file_name": file.filename, "file_size": len(content)})

View File

@ -63,10 +63,6 @@ class Settings(BaseSettings):
aliyun_ai_region: str = Field(default="cn-hangzhou", alias="ALIYUN_AI_REGION")
aliyun_ai_access_key_id: str = Field(default="xxx", alias="ALIYUN_AI_ACCESS_KEY_ID")
aliyun_ai_access_key_secret: str = Field(default="xxx", alias="ALIYUN_AI_ACCESS_KEY_SECRET")
aliyun_ocr_model: str = Field(default="default", alias="ALIYUN_OCR_MODEL")
aliyun_ocr_endpoint: str = Field(default="", alias="ALIYUN_OCR_ENDPOINT")
aliyun_ocr_path: str = Field(default="", alias="ALIYUN_OCR_PATH")
aliyun_ocr_app_code: str = Field(default="", alias="ALIYUN_OCR_APPCODE")
logistics_trace_provider: str = Field(default="internal", alias="LOGISTICS_TRACE_PROVIDER")
logistics_trace_endpoint: str = Field(default="", alias="LOGISTICS_TRACE_ENDPOINT")
logistics_trace_path: str = Field(default="", alias="LOGISTICS_TRACE_PATH")
@ -85,6 +81,7 @@ class Settings(BaseSettings):
wechat_template_inactive_customer: str = Field(default="", alias="WECHAT_TEMPLATE_INACTIVE_CUSTOMER")
llm_parse_enabled: bool = Field(default=True, alias="LLM_PARSE_ENABLED")
llm_parse_model: str = Field(default="qwen-plus", alias="LLM_PARSE_MODEL")
llm_parse_api_key: str = Field(default="", alias="LLM_PARSE_API_KEY")
llm_parse_api_url: str = Field(
default="https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
alias="LLM_PARSE_API_URL",

View File

@ -11,6 +11,8 @@ from fastapi import FastAPI
from fastapi import Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from backend.app.api.routes import api_router
from backend.app.core.config import get_settings
@ -68,3 +70,8 @@ async def unhandled_exception_handler(_: Request, exc: Exception) -> JSONRespons
# 挂载所有 API 路由,路由前缀在 router.py 中统一定义
app.include_router(api_router)
# 挂载本地上传文件目录为静态文件服务(开发环境用)
_local_upload_dir = Path("D:/tmp/order-flow-oss/local-uploads")
_local_upload_dir.mkdir(parents=True, exist_ok=True)
app.mount("/uploads", StaticFiles(directory=str(_local_upload_dir)), name="local-uploads")

View File

@ -535,10 +535,21 @@ class MockOCRProvider(BaseOCRProvider):
返回
三元组 (模拟原始结果, 模拟建议结果, 0.92)
"""
# 模拟 OCR 识别出的文本行,用于开发调试时走通完整解析流程
sample_lines = [
"张三 13800138000",
"广东省深圳市南山区科技园路88号",
"产品名称:工业级密封圈",
"规格DN50 数量100件 单价15.5",
"合计金额1550.00",
"备注:尽快发货",
]
suggested_result = {
"image_name": image_url.rsplit("/", 1)[-1],
"biz_type": biz_type,
"biz_id": biz_id,
"line_list": sample_lines,
"recognized_text": "\n".join(sample_lines),
}
raw_result = {
"provider": self.provider_name,
@ -549,13 +560,15 @@ class MockOCRProvider(BaseOCRProvider):
class AliyunOCRProvider(BaseOCRProvider):
"""阿里云 OCR 提供者,通过 HTTP 接口调用阿里云文字识别服务。
"""阿里云 OCR 提供者,通过官方 SDK 调用阿里云文字识别服务。
依赖 settings 中的 aliyun_ocr_endpointaliyun_ocr_pathaliyun_ocr_app_code
aliyun_ocr_modelaliyun_ai_region 等配置
依赖 settings 中的 aliyun_ai_access_key_idaliyun_ai_access_key_secret
aliyun_ai_region 等配置使用 alibabacloud_ocr_api20210707 SDK
"""
provider_name = "aliyun_ocr"
ENDPOINT = "ocr-api.cn-hangzhou.aliyuncs.com"
def __init__(self, settings) -> None:
self.settings = settings
@ -570,11 +583,9 @@ class AliyunOCRProvider(BaseOCRProvider):
返回
三元组 (原始 API 响应, 结构化建议结果, 置信度)
"""
self._ensure_configured()
payload = self._call_aliyun(image_url)
raw_result = {
"provider": self.provider_name,
"model": self.settings.aliyun_ocr_model,
"region": self.settings.aliyun_ai_region,
"image_url": image_url,
"payload": payload,
@ -583,82 +594,76 @@ class AliyunOCRProvider(BaseOCRProvider):
confidence = self._extract_confidence(payload)
return raw_result, suggested_result, confidence
def _ensure_configured(self) -> None:
"""检查阿里云 OCR 所需配置是否完整。
def _get_client(self):
"""构建阿里云 OCR SDK 客户端。"""
from alibabacloud_ocr_api20210707.client import Client as OcrClient
from alibabacloud_tea_openapi.models import Config
缺少必要配置时抛出 AppExceptionTHIRD_PARTY_FAILED
"""
missing_fields: list[str] = []
if not self.settings.aliyun_ocr_endpoint.strip():
missing_fields.append("ALIYUN_OCR_ENDPOINT")
if not self.settings.aliyun_ocr_path.strip():
missing_fields.append("ALIYUN_OCR_PATH")
if not self.settings.aliyun_ocr_app_code.strip():
missing_fields.append("ALIYUN_OCR_APPCODE")
if missing_fields:
raise AppException(
code=ErrorCode.THIRD_PARTY_FAILED,
message=f"阿里云 OCR 配置不完整:缺少 {', '.join(missing_fields)}",
status_code=400,
)
config = Config(
access_key_id=self.settings.aliyun_ai_access_key_id,
access_key_secret=self.settings.aliyun_ai_access_key_secret,
endpoint=self.ENDPOINT,
)
return OcrClient(config)
def _call_aliyun(self, image_url: str) -> dict:
"""通过 HTTP POST 调用阿里云 OCR 接口。
"""通过官方 SDK 调用阿里云 OCR 接口。
先从 URL 下载图片字节再通过 SDK 发送识别请求
参数
image_url: 图片 URL 地址
返回
阿里云 OCR API JSON 响应字典
阿里云 OCR SDK 响应字典
异常
网络错误HTTP 错误或响应格式异常时抛出 AppException
下载图片失败或 SDK 调用失败时抛出 AppException
"""
# 这里统一走可配置 HTTP 接口,便于后续替换成官方 SDK而不用改业务服务层。
endpoint = self.settings.aliyun_ocr_endpoint.strip().rstrip("/")
api_path = self.settings.aliyun_ocr_path.strip()
if not api_path.startswith("/"):
api_path = f"/{api_path}"
url = f"{endpoint}{api_path}"
body = json.dumps(
{
"url": image_url,
"image_url": image_url,
"model": self.settings.aliyun_ocr_model,
},
ensure_ascii=False,
).encode("utf-8")
headers = {
"Content-Type": "application/json; charset=UTF-8",
"Authorization": f"APPCODE {self.settings.aliyun_ocr_app_code.strip()}",
}
req = request.Request(url=url, data=body, headers=headers, method="POST")
from alibabacloud_ocr_api20210707.models import RecognizeGeneralRequest
from urllib import request as urllib_request, error as urllib_error
from urllib.parse import urlparse, quote, urlunparse
# 对 URL 中的非 ASCII 字符(如中文文件名)进行编码,避免 ASCII 编码异常
parsed = urlparse(image_url)
safe_url = urlunparse(parsed._replace(path=quote(parsed.path)))
try:
with request.urlopen(req, timeout=20) as response:
response_text = response.read().decode("utf-8")
except error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="ignore")
with urllib_request.urlopen(safe_url, timeout=15) as resp:
image_bytes = resp.read()
except (urllib_error.HTTPError, urllib_error.URLError) as exc:
raise AppException(
code=ErrorCode.THIRD_PARTY_FAILED,
message=f"阿里云 OCR 调用失败HTTP {exc.code} {detail}".strip(),
status_code=400,
) from exc
except error.URLError as exc:
raise AppException(
code=ErrorCode.THIRD_PARTY_FAILED,
message=f"阿里云 OCR 网络请求失败:{exc.reason}",
message=f"下载 OCR 图片失败:{exc}",
status_code=400,
) from exc
try:
payload = json.loads(response_text) if response_text else {}
except json.JSONDecodeError as exc:
client = self._get_client()
ocr_request = RecognizeGeneralRequest(body=image_bytes)
response = client.recognize_general(ocr_request)
except Exception as exc:
raise AppException(
code=ErrorCode.THIRD_PARTY_FAILED,
message="阿里云 OCR 返回了无法解析的响应",
message=f"阿里云 OCR SDK 调用失败:{exc}",
status_code=400,
) from exc
if not isinstance(payload, dict):
raise AppException(code=ErrorCode.THIRD_PARTY_FAILED, message="阿里云 OCR 返回格式异常", status_code=400)
body = response.body
if hasattr(body, "to_map"):
payload = body.to_map()
elif isinstance(body, dict):
payload = body
else:
payload = json.loads(str(body)) if body else {}
# SDK 返回 Data 为 JSON 字符串,解析为 dict 便于后续统一提取
if isinstance(payload.get("Data"), str):
try:
payload["Data"] = json.loads(payload["Data"])
except (json.JSONDecodeError, TypeError):
pass
return payload
def _build_suggested_result(self, payload: dict, image_url: str, biz_type: str, biz_id: int) -> dict:
@ -978,7 +983,7 @@ class AIService:
llm_result = llm_parser.safe_parse(
raw_text,
product_groups,
settings.aliyun_ai_access_key_id,
settings.llm_parse_api_key or settings.aliyun_ai_access_key_id,
settings.llm_parse_api_url,
ocr_context=ocr_context,
)

View File

@ -41,7 +41,7 @@ class AuthService:
}
DEFAULT_ROLE_PERMISSIONS = {
"salesman": ["order:create", "order:list", "order:submit", "customer:list"],
"salesman": ["order:create", "order:list", "order:submit", "customer:list", "ai:parse-order"],
"manager": [
"order:list",
"order:approve",

View File

@ -102,6 +102,7 @@ DEFAULT_MENUS = [
{"id": 35, "parent_id": 34, "menu_name": "AI结果修正", "menu_path": "", "menu_type": "button", "permission_code": "ai:correct", "icon": "", "sort_no": 34, "status": 1},
{"id": 36, "parent_id": 4, "menu_name": "定价规则查看", "menu_path": "", "menu_type": "button", "permission_code": "master-data:list", "icon": "", "sort_no": 35, "status": 1},
{"id": 37, "parent_id": 4, "menu_name": "定价规则编辑", "menu_path": "", "menu_type": "button", "permission_code": "master-data:update", "icon": "", "sort_no": 36, "status": 1},
{"id": 38, "parent_id": 0, "menu_name": "AI订单解析", "menu_path": "/ai/parse-order", "menu_type": "page", "permission_code": "ai:parse-order", "icon": "ai-parse", "sort_no": 37, "status": 1},
]
# ---------------------------------------------------------------------------
@ -124,7 +125,7 @@ DEFAULT_ADMIN_USER = {
DEFAULT_ROLE_MENU_CODES = {
"admin": [item["permission_code"] for item in DEFAULT_MENUS],
"salesman": ["order:list", "order:create", "order:submit", "customer:list", "reminder:list"],
"salesman": ["order:list", "order:create", "order:submit", "customer:list", "reminder:list", "ai:parse-order"],
"manager": [
"order:approve",
"order:cancel-approve",

View File

@ -6,3 +6,5 @@ sqlalchemy==2.0.40
alembic==1.15.2
pymysql==1.1.1
openpyxl==3.1.5
alibabacloud-ocr-api20210707==3.1.0
alibabacloud-tea-openapi==0.5.2

78
fix_missing_columns.py Normal file
View File

@ -0,0 +1,78 @@
"""
数据库列修复脚本
自动检测 ORM 模型与实际数据库表的列差异仅添加缺失的列
用法: python fix_missing_columns.py
"""
import pymysql
# 数据库连接配置(从 .env 读取)
DB_CONFIG = {
"host": "fn.taisan.online",
"port": 33306,
"user": "root",
"password": "taiyi1224",
"database": "order_flow",
"charset": "utf8mb4",
}
# 需要检查并修复的表和列定义
# 格式: { "表名": [ (列名, 列定义SQL), ... ] }
TABLE_FIXES = {
"product": [
("pricing_type", "VARCHAR(32) NULL DEFAULT NULL COMMENT '计价方式area/weight'"),
("pricing_unit", "VARCHAR(16) NULL DEFAULT '' COMMENT '计价单位'"),
("thickness", "VARCHAR(32) NULL DEFAULT NULL COMMENT '厚度'"),
("weight_gsm", "INT NULL DEFAULT NULL COMMENT '克重(gsm)'"),
("default_width_m", "DECIMAL(10,4) NULL DEFAULT NULL COMMENT '默认宽度(米)'"),
("is_default", "INT NOT NULL DEFAULT 0 COMMENT '是否默认规格0否 1是'"),
],
"sales_order_item": [
("pricing_type", "VARCHAR(32) NULL DEFAULT NULL COMMENT '计价方式area/weight'"),
("length_m", "DECIMAL(10,4) NULL DEFAULT NULL COMMENT '长度(米)'"),
("width_m", "DECIMAL(10,4) NULL DEFAULT NULL COMMENT '宽度(米)'"),
("area_sqm", "DECIMAL(18,4) NULL DEFAULT NULL COMMENT '面积(平方米)'"),
("surcharge_detail", "TEXT NULL DEFAULT NULL COMMENT '附加费用明细'"),
("processing_detail", "TEXT NULL DEFAULT NULL COMMENT '加工费用明细'"),
("supplier_id", "INT NULL DEFAULT NULL COMMENT '供应商ID'"),
("supplier_model", "VARCHAR(64) NULL DEFAULT NULL COMMENT '供应商型号'"),
("price_tier", "VARCHAR(32) NULL DEFAULT NULL COMMENT '价格层级'"),
],
}
def get_existing_columns(cursor, table_name: str) -> set[str]:
"""获取表中已存在的列名集合。"""
cursor.execute(f"SHOW COLUMNS FROM `{table_name}`")
return {row[0] for row in cursor.fetchall()}
def main():
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor()
total_added = 0
for table_name, columns in TABLE_FIXES.items():
existing = get_existing_columns(cursor, table_name)
print(f"\n[{table_name}] 已有 {len(existing)} 列: {', '.join(sorted(existing))}")
for col_name, col_def in columns:
if col_name in existing:
print(f"{col_name} 已存在,跳过")
else:
sql = f"ALTER TABLE `{table_name}` ADD COLUMN `{col_name}` {col_def}"
try:
cursor.execute(sql)
conn.commit()
print(f" + {col_name} 已添加")
total_added += 1
except Exception as e:
conn.rollback()
print(f"{col_name} 添加失败: {e}")
print(f"\n完成!共添加 {total_added} 个缺失列。")
cursor.close()
conn.close()
if __name__ == "__main__":
main()

28
fix_missing_columns.sql Normal file
View File

@ -0,0 +1,28 @@
-- 修复 ORM 模型与实际数据库表结构不一致的问题
-- 问题ORM 模型定义了多个列,但数据库表中不存在,导致 SQLAlchemy 查询报 Unknown column 错误
-- 执行日期2026-05-31
-- ============================================
-- 1. product 表:添加 ORM 模型中存在但数据库缺失的 6 个列
-- ============================================
ALTER TABLE `product`
ADD COLUMN `pricing_type` varchar(32) NULL DEFAULT NULL COMMENT '计价方式area/weight' AFTER `sale_price`,
ADD COLUMN `pricing_unit` varchar(16) NULL DEFAULT '' COMMENT '计价单位' AFTER `pricing_type`,
ADD COLUMN `thickness` varchar(32) NULL DEFAULT NULL COMMENT '厚度' AFTER `pricing_unit`,
ADD COLUMN `weight_gsm` int NULL DEFAULT NULL COMMENT '克重(gsm)' AFTER `thickness`,
ADD COLUMN `default_width_m` decimal(10,4) NULL DEFAULT NULL COMMENT '默认宽度(米)' AFTER `weight_gsm`,
ADD COLUMN `is_default` int NOT NULL DEFAULT 0 COMMENT '是否默认规格0否 1是' AFTER `status`;
-- ============================================
-- 2. sales_order_item 表:添加 ORM 模型中存在但数据库缺失的 8 个列
-- ============================================
ALTER TABLE `sales_order_item`
ADD COLUMN `pricing_type` varchar(32) NULL DEFAULT NULL COMMENT '计价方式area/weight' AFTER `other_fee_amount`,
ADD COLUMN `length_m` decimal(10,4) NULL DEFAULT NULL COMMENT '长度(米)' AFTER `pricing_type`,
ADD COLUMN `width_m` decimal(10,4) NULL DEFAULT NULL COMMENT '宽度(米)' AFTER `length_m`,
ADD COLUMN `area_sqm` decimal(18,4) NULL DEFAULT NULL COMMENT '面积(平方米)' AFTER `width_m`,
ADD COLUMN `surcharge_detail` text NULL DEFAULT NULL COMMENT '附加费用明细' AFTER `area_sqm`,
ADD COLUMN `processing_detail` text NULL DEFAULT NULL COMMENT '加工费用明细' AFTER `surcharge_detail`,
ADD COLUMN `supplier_id` int NULL DEFAULT NULL COMMENT '供应商ID' AFTER `processing_detail`,
ADD COLUMN `supplier_model` varchar(64) NULL DEFAULT NULL COMMENT '供应商型号' AFTER `supplier_id`,
ADD COLUMN `price_tier` varchar(32) NULL DEFAULT NULL COMMENT '价格层级' AFTER `supplier_model`;

View File

@ -16,8 +16,8 @@ App({
},
onLaunch: function () {
var sysInfo = wx.getSystemInfoSync();
this.globalData.statusBarHeight = sysInfo.statusBarHeight || 20;
var windowInfo = wx.getWindowInfo();
this.globalData.statusBarHeight = windowInfo.statusBarHeight || 20;
this.globalData.navBarHeight = 44;
var token = auth.getToken();

View File

@ -16,5 +16,15 @@
"navigationBarTitleText": "订单全流程",
"navigationStyle": "custom"
},
"custom-tab-bar": "custom-tab-bar"
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/driver/task-list/task-list", "text": "任务" },
{ "pagePath": "pages/driver/order-tracking/order-tracking", "text": "物流" },
{ "pagePath": "pages/manager/dashboard/dashboard", "text": "首页" },
{ "pagePath": "pages/manager/approve-list/approve-list", "text": "审批" },
{ "pagePath": "pages/manager/order-list/order-list", "text": "订单" },
{ "pagePath": "pages/my/my", "text": "我的" }
]
}
}

View File

@ -2,7 +2,7 @@
* 司机端物流追踪页面
* 职责展示司机最近的任务列表支持查看关联订单的物流轨迹详情
*/
var auth = require("../../utils/auth");
var auth = require("../../../utils/auth");
Page({
data: {

View File

@ -18,6 +18,7 @@
color: #0f172a;
}
/* Status banner */
.status-banner {
border-radius: 20rpx;
padding: 28rpx;
@ -68,6 +69,7 @@
margin-top: 6rpx;
}
/* Address card */
.addr-card {
padding: 24rpx;
}
@ -115,6 +117,7 @@
line-height: 1.5;
}
/* Cargo card */
.cargo-card {
padding: 20rpx 24rpx;
}
@ -148,6 +151,7 @@
font-size: 30rpx;
}
/* Photo card */
.photo-card {
padding: 24rpx;
}
@ -219,6 +223,7 @@
color: #94a3b8;
}
/* Trace */
.trace-card {
padding: 20rpx 24rpx;
}
@ -257,6 +262,7 @@
margin-top: 2rpx;
}
/* Sticky bottom action bar */
.action-bar {
position: fixed;
left: 0;

View File

@ -133,6 +133,7 @@
padding: 20rpx 24rpx;
}
/* Sticky bottom action bar */
.action-bar {
position: fixed;
left: 0;
@ -168,6 +169,7 @@
border: 2rpx solid #fecdd3;
}
/* 报价明细卡片 */
.pricing-card { padding: 24rpx; }
.pricing-item { margin-bottom: 20rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #e5eaf1; }
.pricing-item:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }

View File

@ -60,18 +60,6 @@
color: #2563eb;
}
.stat-value.orange {
color: #d97706;
}
.stat-value.blue {
color: #2563eb;
}
.stat-value.green {
color: #059669;
}
.stat-label {
font-size: 22rpx;
color: #64748b;
@ -104,14 +92,14 @@
background: #2563eb;
}
.menu-dot.green {
background: #059669;
}
.menu-dot.orange {
background: #f59e0b;
}
.menu-dot.green {
background: #059669;
}
.menu-dot.gray {
background: #94a3b8;
}

View File

@ -32,4 +32,4 @@
"appid": "wxdbddd60e12144043",
"editorSetting": {},
"libVersion": "3.16.0"
}
}

View File

@ -0,0 +1,21 @@
{
"libVersion": "3.16.0",
"projectname": "mini-app",
"setting": {
"urlCheck": false,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"showShadowRootInWxmlPanel": false,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"compileHotReLoad": true,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
}
}

View File

@ -528,3 +528,26 @@ export async function parseOrderImage(imageUrl) {
body: JSON.stringify({ input_type: "image", image_url: imageUrl }),
});
}
/**
* 开发环境本地文件上传绕过 OSS 直传
* @param {File} file - 要上传的文件对象
* @returns {Promise<{url: string, file_name: string, file_size: number}>} 上传结果包含访问 URL
*/
export async function localUploadFile(file) {
const formData = new FormData();
formData.append("file", file);
const token = salesStore.token;
const response = await fetch(`${API_BASE_URL}/api/files/local-upload`, {
method: "POST",
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: formData,
});
const result = await response.json().catch(() => ({}));
if (!response.ok || result.code !== 0) {
throw new Error(result.message || `上传失败: ${response.status}`);
}
return result.data;
}

View File

@ -40,7 +40,7 @@
</div>
<label class="ghost-btn parse-upload-label">
选择图片
<input type="file" accept="image/*" capture="environment" style="display:none" @change="handleParseImageChange" />
<input type="file" accept="image/*" style="display:none" @change="handleParseImageChange" />
</label>
<div class="parse-actions">
<button type="button" class="primary-btn" :disabled="parseLoading || !parseImageFile" @click="handleParseImageFile">
@ -344,11 +344,11 @@ import { useRoute, useRouter } from "vue-router";
import {
createOrder,
createUploadToken,
fetchCustomerOptions,
fetchFactoryOptions,
fetchOrderForEdit,
fetchProductOptions,
localUploadFile,
parseOrderImage,
parseOrderText,
updateOrder,
@ -539,22 +539,9 @@ async function handleParseImageFile() {
parseLoading.value = true;
message.value = "";
try {
const tokenData = await createUploadToken({
file_name: file.name,
file_type: file.type,
file_size: file.size,
biz_type: "order_parse",
biz_id: 0,
});
const formData = new FormData();
formData.append("key", tokenData.object_key);
formData.append("policy", tokenData.policy);
formData.append("OSSAccessKeyId", tokenData.access_key_id);
formData.append("signature", tokenData.signature);
formData.append("file", file);
await fetch(tokenData.upload_url, { method: "POST", body: formData });
const imageUrl = `${tokenData.public_base_url}/${tokenData.object_key}`;
const result = await parseOrderImage(imageUrl);
// OSS
const uploadResult = await localUploadFile(file);
const result = await parseOrderImage(uploadResult.url);
parsedResult.value = result;
parseEditable.value = {
customer_name: result.parsed_order.customer_name || "",
@ -1256,6 +1243,23 @@ button:disabled {
}
/* 智能填单样式 */
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid #d1d5db;
border-radius: 12px;
padding: 10px 14px;
background: #f8fafc;
color: #334155;
cursor: pointer;
font-size: 13px;
}
.ghost-btn:disabled {
color: #9ca3af;
background: #f3f4f6;
cursor: not-allowed;
}
.parse-card { border: 2px dashed #93c5fd; background: linear-gradient(180deg, #f0f9ff, #fff); }
.parse-tabs { display: flex; gap: 8px; margin-bottom: 12px; }
.parse-tabs button { border: 1px solid #d1d5db; background: #fff; border-radius: 10px; padding: 8px 16px; cursor: pointer; font-size: 13px; }