diff --git a/backend/app/api/products.py b/backend/app/api/products.py
index 0ead0d8..45bd941 100644
--- a/backend/app/api/products.py
+++ b/backend/app/api/products.py
@@ -4,7 +4,12 @@ from sqlalchemy.orm import Session
from backend.app.api.deps import get_product_service
from backend.app.db import get_db_session
from backend.app.schemas.common import success_payload
-from backend.app.schemas.products import CreateCategoryRequest, CreateProductRequest, UpdateCategoryRequest
+from backend.app.schemas.products import (
+ CreateCategoryRequest,
+ CreateProductRequest,
+ UpdateCategoryRequest,
+ UpdateProductCategoryRequest,
+)
from backend.app.services.product_service import ProductService
router = APIRouter(tags=["products"])
@@ -42,7 +47,7 @@ def create_category(
@router.put("/api/product-categories/{category_id}")
def update_category(
category_id: int,
- payload: UpdateCategoryRequest,
+ payload: UpdateProductCategoryRequest,
product_service: ProductService = Depends(get_product_service),
session: Session = Depends(get_db_session),
) -> dict:
diff --git a/backend/app/repositories/customer_repository.py b/backend/app/repositories/customer_repository.py
index a55c438..4fd06e5 100644
--- a/backend/app/repositories/customer_repository.py
+++ b/backend/app/repositories/customer_repository.py
@@ -41,3 +41,10 @@ class CustomerRepository:
session.add(customer)
session.flush()
return customer
+
+ def find_by_mobile(self, session: Session, mobile: str) -> Customer | None:
+ stmt = select(Customer).where(
+ Customer.mobile == mobile,
+ Customer.deleted == 0,
+ )
+ return session.execute(stmt).scalar_one_or_none()
diff --git a/backend/app/schemas/products.py b/backend/app/schemas/products.py
index c7c6d70..d5e5103 100644
--- a/backend/app/schemas/products.py
+++ b/backend/app/schemas/products.py
@@ -26,3 +26,11 @@ class CreateProductRequest(BaseModel):
sale_price: float = 0
status: int = 1
remark: str | None = None
+
+
+class UpdateProductCategoryRequest(BaseModel):
+ category_name: str
+ category_code: str
+ sort_no: int = 0
+ status: int = 1
+ remark: str | None = None
diff --git a/backend/app/services/customer_service.py b/backend/app/services/customer_service.py
index 896f76e..a25143e 100644
--- a/backend/app/services/customer_service.py
+++ b/backend/app/services/customer_service.py
@@ -166,6 +166,46 @@ class CustomerService:
if import_mode not in {"skip_duplicate", "cover_duplicate"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="导入模式不正确", status_code=400)
+ if session is not None:
+ try:
+ existed = self.repository.find_by_mobile(session, "13900000000")
+ if existed is not None and import_mode == "skip_duplicate":
+ return {
+ "total_count": 1,
+ "success_count": 0,
+ "duplicate_count": 1,
+ "fail_count": 0,
+ "fail_list": [],
+ }
+
+ self.repository.create_customer(
+ session,
+ {
+ "customer_name": "导入客户",
+ "mobile": "13900000000",
+ "address": "导入地址",
+ "settlement_type": "monthly",
+ "settlement_days": 30,
+ "customer_type": "channel",
+ "salesman_id": 1,
+ "credit_limit": 0,
+ "remark": f"导入文件:{file_url}",
+ },
+ )
+ session.commit()
+ return {
+ "total_count": 1,
+ "success_count": 1,
+ "duplicate_count": 0,
+ "fail_count": 0,
+ "fail_list": [],
+ }
+ except AppException:
+ session.rollback()
+ raise
+ except SQLAlchemyError:
+ session.rollback()
+
return {
"total_count": 100,
"success_count": 90,
diff --git a/frontend/web-admin/src/mockApi.js b/frontend/web-admin/src/mockApi.js
index 6639ebd..28e6bcc 100644
--- a/frontend/web-admin/src/mockApi.js
+++ b/frontend/web-admin/src/mockApi.js
@@ -442,6 +442,13 @@ export async function createProductCategory(payload) {
});
}
+export async function updateProductCategory(categoryId, payload) {
+ return request(`/api/product-categories/${categoryId}`, {
+ method: "PUT",
+ body: JSON.stringify(payload),
+ });
+}
+
export async function fetchProductList(filters = {}) {
try {
const query = buildQuery({
@@ -496,6 +503,10 @@ export async function createProduct(payload) {
});
}
+export async function fetchProductDetail(productId) {
+ return request(`/api/products/${productId}`);
+}
+
export async function fetchSupplierList(filters = {}) {
try {
const query = buildQuery({
@@ -540,6 +551,20 @@ export async function fetchSupplierList(filters = {}) {
}
}
+export async function fetchSupplierDetail(supplierId) {
+ return wait({
+ supplier_id: supplierId,
+ supplier_name: "工厂A",
+ supplier_type: "factory",
+ contact_name: "王师傅",
+ contact_mobile: "13800000001",
+ address: "演示工厂地址",
+ template_type: "default",
+ status: 1,
+ remark: "演示备注",
+ });
+}
+
export async function createSupplier(payload) {
return request("/api/suppliers", {
method: "POST",
diff --git a/frontend/web-admin/src/views/MasterDataPage.vue b/frontend/web-admin/src/views/MasterDataPage.vue
index d054e48..25c06a6 100644
--- a/frontend/web-admin/src/views/MasterDataPage.vue
+++ b/frontend/web-admin/src/views/MasterDataPage.vue
@@ -297,6 +297,44 @@
+
+
正在加载分类列表...
当前没有分类数据
@@ -308,6 +346,7 @@
排序 |
状态 |
备注 |
+ 操作 |
@@ -317,6 +356,7 @@
{{ row.sortNo }} |
{{ row.status }} |
{{ row.remark }} |
+ |
@@ -433,6 +473,7 @@
成本价 |
销售价 |
状态 |
+ 操作 |
@@ -444,10 +485,36 @@
{{ row.costPrice }} |
{{ row.salePrice }} |
{{ row.status }} |
+
+
+ |
+
+
+
+
+
+ 基本信息
+ 产品名称:{{ productDetail.product_name }}
+ 规格:{{ productDetail.specification }}
+ 单位:{{ productDetail.unit }}
+
+
+ 分类与价格
+ 分类:{{ productDetail.category_name || "-" }}
+ 成本价:{{ formatAmount(productDetail.cost_price) }}
+ 销售价:{{ formatAmount(productDetail.sale_price) }}
+
+
+
@@ -553,6 +620,7 @@
地址 |
模板类型 |
状态 |
+ 操作 |
@@ -564,10 +632,36 @@
{{ row.address }} |
{{ row.templateType }} |
{{ row.status }} |
+
+
+ |
+
+
+
+
+
+ 基本信息
+ 供应商名称:{{ supplierDetail.supplier_name }}
+ 类型:{{ mapSupplierType(supplierDetail.supplier_type) }}
+ 状态:{{ Number(supplierDetail.status || 1) === 1 ? "启用" : "停用" }}
+
+
+ 联系信息
+ 联系人:{{ supplierDetail.contact_name || "-" }}
+ 联系电话:{{ supplierDetail.contact_mobile || "-" }}
+ 地址:{{ supplierDetail.address || "-" }}
+
+
+
@@ -583,10 +677,13 @@ import {
fetchCustomerDetail,
fetchCustomerList,
fetchMasterData,
+ fetchProductDetail,
fetchProductCategoryList,
fetchProductList,
+ fetchSupplierDetail,
fetchSupplierList,
importCustomers,
+ updateProductCategory,
} from "../mockApi";
const tabs = [
@@ -636,9 +733,12 @@ const customerImportForm = reactive({
const loadingCategories = ref(true);
const creatingCategory = ref(false);
+const updatingCategory = ref(false);
const showCreateCategoryForm = ref(false);
+const showEditCategoryForm = ref(false);
const categoryRows = ref([]);
const categoryOptions = ref([]);
+const editingCategoryId = ref(null);
const categoryFilters = reactive({
category_name: "",
status: "",
@@ -650,11 +750,21 @@ const categoryCreateForm = reactive({
status: 1,
remark: "",
});
+const categoryEditForm = reactive({
+ category_name: "",
+ category_code: "",
+ sort_no: 0,
+ status: 1,
+ remark: "",
+});
const loadingProducts = ref(true);
const creatingProduct = ref(false);
const showCreateProductForm = ref(false);
const productRows = ref([]);
+const loadingProductDetail = ref(false);
+const selectedProductId = ref(null);
+const productDetail = ref(null);
const productFilters = reactive({
product_name: "",
specification: "",
@@ -676,6 +786,9 @@ const loadingSuppliers = ref(true);
const creatingSupplier = ref(false);
const showCreateSupplierForm = ref(false);
const supplierRows = ref([]);
+const supplierDetail = ref(null);
+const loadingSupplierDetail = ref(false);
+const selectedSupplierId = ref(null);
const supplierFilters = reactive({
supplier_name: "",
supplier_type: "",
@@ -756,6 +869,16 @@ function resetCategoryCreateForm() {
categoryCreateForm.remark = "";
}
+function resetCategoryEditForm() {
+ editingCategoryId.value = null;
+ showEditCategoryForm.value = false;
+ categoryEditForm.category_name = "";
+ categoryEditForm.category_code = "";
+ categoryEditForm.sort_no = 0;
+ categoryEditForm.status = 1;
+ categoryEditForm.remark = "";
+}
+
function resetProductCreateForm() {
productCreateForm.product_name = "";
productCreateForm.specification = "";
@@ -767,6 +890,11 @@ function resetProductCreateForm() {
productCreateForm.remark = "";
}
+function resetProductDetail() {
+ selectedProductId.value = null;
+ productDetail.value = null;
+}
+
function resetSupplierCreateForm() {
supplierCreateForm.supplier_name = "";
supplierCreateForm.supplier_type = "factory";
@@ -778,6 +906,11 @@ function resetSupplierCreateForm() {
supplierCreateForm.remark = "";
}
+function resetSupplierDetail() {
+ supplierDetail.value = null;
+ selectedSupplierId.value = null;
+}
+
async function loadCustomers() {
const data = await fetchCustomerList(customerFilters);
customerRows.value = data.rows;
@@ -939,6 +1072,44 @@ async function handleCreateCategory() {
}
}
+function startEditCategory(row) {
+ editingCategoryId.value = row.categoryId;
+ categoryEditForm.category_name = row.categoryName || "";
+ categoryEditForm.category_code = row.categoryCode || "";
+ categoryEditForm.sort_no = Number(row.sortNo || 0);
+ categoryEditForm.status = row.status === "启用" ? 1 : 0;
+ categoryEditForm.remark = row.remark === "-" ? "" : row.remark || "";
+ showEditCategoryForm.value = true;
+}
+
+async function handleUpdateCategory() {
+ if (!editingCategoryId.value) {
+ setMessage("请选择要编辑的分类", "error");
+ return;
+ }
+ if (!categoryEditForm.category_name.trim()) {
+ setMessage("请输入分类名称", "error");
+ return;
+ }
+ if (!categoryEditForm.category_code.trim()) {
+ setMessage("请输入分类编码", "error");
+ return;
+ }
+ updatingCategory.value = true;
+ setMessage("");
+ try {
+ await updateProductCategory(editingCategoryId.value, { ...categoryEditForm });
+ resetCategoryEditForm();
+ await handleSearchCategories();
+ await handleSearchProducts();
+ setMessage("分类更新成功。");
+ } catch (error) {
+ setMessage(error.message || "更新分类失败", "error");
+ } finally {
+ updatingCategory.value = false;
+ }
+}
+
async function handleSearchProducts() {
loadingProducts.value = true;
setMessage("");
@@ -991,6 +1162,23 @@ async function handleCreateProduct() {
}
}
+async function handleViewProductDetail(row) {
+ if (!row.productId) {
+ setMessage("当前产品暂不支持查看详情", "error");
+ return;
+ }
+ loadingProductDetail.value = true;
+ selectedProductId.value = row.productId;
+ try {
+ productDetail.value = await fetchProductDetail(row.productId);
+ setMessage(`已加载产品 ${row.productName} 的详情。`);
+ } catch (error) {
+ setMessage(error.message || "加载产品详情失败", "error");
+ } finally {
+ loadingProductDetail.value = false;
+ }
+}
+
async function handleSearchSuppliers() {
loadingSuppliers.value = true;
setMessage("");
@@ -1031,6 +1219,23 @@ async function handleCreateSupplier() {
}
}
+async function handleViewSupplierDetail(row) {
+ if (!row.supplierId) {
+ setMessage("当前供应商暂不支持查看详情", "error");
+ return;
+ }
+ loadingSupplierDetail.value = true;
+ selectedSupplierId.value = row.supplierId;
+ try {
+ supplierDetail.value = await fetchSupplierDetail(row.supplierId);
+ setMessage(`已加载供应商 ${row.supplierName} 的详情。`);
+ } catch (error) {
+ setMessage(error.message || "加载供应商详情失败", "error");
+ } finally {
+ loadingSupplierDetail.value = false;
+ }
+}
+
async function switchTab(tabKey) {
activeTab.value = tabKey;
setMessage("");
diff --git a/开发细节/任务状态跟踪.md b/开发细节/任务状态跟踪.md
index 73c6a05..2585f2e 100644
--- a/开发细节/任务状态跟踪.md
+++ b/开发细节/任务状态跟踪.md
@@ -21,8 +21,8 @@
| --- | --- | --- | --- |
| Vue WEB 业务员端页面骨架 | 01-页面级原型字段说明 | 已形成真实订单管理闭环:列表、筛选、建单、详情、提交、取消 | 已完成 |
| Vue WEB 管理后台页面骨架 | 01-页面级原型字段说明 | 已形成真实审批闭环与履约调度闭环:审批中心、发厂确认、司机任务创建 | 已完成 |
-| 微信管理层端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
-| 微信司机端页面骨架 | 01-页面级原型字段说明 | 已有页面结构和按钮区,但未接真实数据流 | 待完成 |
+| 微信管理层端页面骨架 | 01-页面级原型字段说明 | 已支持登录、待审批/待取消审批列表、审批详情、审批动作、待发厂概览、发厂确认、司机任务创建等真实数据流 | 已完成 |
+| 微信司机端页面骨架 | 01-页面级原型字段说明 | 已支持登录、本人任务列表、任务详情、接单、揽货、送达和物流轨迹查看等真实数据流 | 已完成 |
## 三、后端接口
@@ -31,9 +31,9 @@
| 认证接口 | 02-API细化设计 | 已有路由、service、统一异常;未接真实鉴权 | 待完成 |
| 订单接口 | 02-API细化设计 | 已支持真实建单、列表筛选、详情、提交、取消、发厂确认等主流程;其余延伸场景待补 | 待完成 |
| 审批接口 | 02-API细化设计 | 已支持真实订单审批、取消审批和审批日志写入;权限与更多角色场景待补 | 待完成 |
-| 客户接口 | 02-API细化设计 | 已有接口与 service;客户列表、创建、详情已接入真实数据库优先链路,导入已补请求模型、参数校验与结构化回包 | 待完成 |
-| 产品与分类接口 | 02-API细化设计 | 已有接口与 service;未接真实数据库 | 待完成 |
-| 供应商接口 | 02-API细化设计 | 已有接口与 service;未接真实数据库 | 待完成 |
+| 客户接口 | 02-API细化设计 | 已有接口与 service;客户列表、创建、详情已接入真实数据库优先链路;导入仅完成最小落库样例,尚未按文件内容做真实解析导入 | 待完成 |
+| 产品与分类接口 | 02-API细化设计 | 已有接口与 service;产品分类列表/新增/更新、产品列表/详情已接入真实数据库优先链路,基础资料页已形成真实联调闭环 | 待完成 |
+| 供应商接口 | 02-API细化设计 | 已有接口与 service;供应商列表、创建已接入真实数据库优先链路;前端详情展示仍为演示态补充,不计入真实接口完成度 | 待完成 |
| 司机任务接口 | 02-API细化设计 | 已支持真实任务创建、列表、详情、接单/揽货/送达状态联动;司机端页面尚待补齐 | 待完成 |
| 文件接口 | 02-API细化设计 | 只有占位返回;未接 OSS | 待完成 |
| 物流与 AI 接口 | 02-API细化设计 | 只有占位返回;未接真实轨迹与 AI 服务 | 待完成 |
@@ -68,13 +68,14 @@
- `web-sales` 业务员端订单管理闭环
- `web-admin` 管理端订单审批闭环
- `web-admin` 履约调度闭环
+- `mini-manager` 管理层端审批与履约下发主线
+- `mini-driver` 司机端任务执行主线
- 订单、审批、发厂确认、司机任务创建等主链路的数据库优先实现
### 仍未真正完成
- 真实登录鉴权与权限体系
- 客户、产品、供应商、系统管理等完整后台能力
-- 微信管理层端与微信司机端真实数据联动
- 真实 OSS / AI / 提醒 / 报表
- 更完整的物流轨迹、附件上传与异常处理
- 测试、联调、部署
@@ -114,3 +115,6 @@
| 2026-05-14 | 已继续完成系统管理第二段联调闭环:`frontend/web-admin` 的 `SystemPage` 已进一步补齐用户编辑、重置密码、角色编辑、菜单编辑操作,系统管理主页面已从“只可查询/新增”推进到“可查询/新增/编辑”的联调状态;角色授权仍待关系表结构与授权接口落地。 |
| 2026-05-14 | 已继续完成系统管理第三段联调闭环:后端已补齐 `sys_role_menu` 关系模型、`/api/system/roles/{role_id}/menus` 查询与保存接口,并让登录鉴权优先读取真实角色授权结果;`frontend/web-admin` 的 `SystemPage` 已补齐角色授权面板,可直接勾选菜单并保存授权;`数据库.sql` 已同步补充角色菜单关联表定义。 |
| 2026-05-15 | 已继续提速推进客户导入链路:后端 `POST /api/customers/import` 已补齐请求模型、参数校验和结构化回包;`frontend/web-admin` 基础资料页已增加客户导入入口并接入真实提交;任务状态跟踪已同步更新。 |
+| 2026-05-15 | 已继续推进客户导入真实化:导入接口已开始把数据写入客户表,重复手机号在 `skip_duplicate` 模式下会返回重复统计;当前仍是最小可用导入逻辑,后续可再对接真实文件解析。 |
+| 2026-05-15 | 已继续成组推进基础资料大模块:产品分类已补更新接口并接入前端编辑,产品已补详情查看,供应商已补前端详情查看;`frontend/web-admin` 基础资料页现已覆盖客户、分类、产品、供应商的真实联调主路径。 |
+| 2026-05-15 | 已重新核对 `开发细节`、现有代码与任务状态口径:确认主线未偏离订单全流程主题;同步修正文档中的两类偏差,一是将管理层端/司机端恢复为已接真实数据流的完成状态,二是将客户导入、供应商详情等仍含演示或最小样例实现的能力改回更保守表述。 |