Compare commits
2 Commits
8640290ab4
...
272d97488d
| Author | SHA1 | Date | |
|---|---|---|---|
| 272d97488d | |||
| 0e7b5e0ae1 |
@ -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 Key(DashScope 通义千问)
|
||||
LLM_PARSE_API_KEY=
|
||||
|
||||
# ==================== 物流轨迹查询 ====================
|
||||
|
||||
|
||||
@ -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)})
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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_endpoint、aliyun_ocr_path、aliyun_ocr_app_code、
|
||||
aliyun_ocr_model、aliyun_ai_region 等配置。
|
||||
依赖 settings 中的 aliyun_ai_access_key_id、aliyun_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
|
||||
|
||||
缺少必要配置时抛出 AppException(THIRD_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,
|
||||
)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
78
fix_missing_columns.py
Normal 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
28
fix_missing_columns.sql
Normal 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`;
|
||||
@ -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();
|
||||
|
||||
@ -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": "我的" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* 司机端物流追踪页面
|
||||
* 职责:展示司机最近的任务列表,支持查看关联订单的物流轨迹详情。
|
||||
*/
|
||||
var auth = require("../../utils/auth");
|
||||
var auth = require("../../../utils/auth");
|
||||
|
||||
Page({
|
||||
data: {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -32,4 +32,4 @@
|
||||
"appid": "wxdbddd60e12144043",
|
||||
"editorSetting": {},
|
||||
"libVersion": "3.16.0"
|
||||
}
|
||||
}
|
||||
21
frontend/mini-app/project.private.config.json
Normal file
21
frontend/mini-app/project.private.config.json
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user