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 @@ +
+
+

编辑分类

+ 调用真实 `/api/product-categories/{category_id}` +
+
+ + + + + +
+
+ + +
+
+

正在加载分类列表...

当前没有分类数据
@@ -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 }} + + +
+ +
+
+

产品详情

+ 产品 ID:{{ productDetail.product_id }} +
+
+
+
基本信息
+

产品名称:{{ 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 }} + + + + +
+
+

供应商详情

+ 供应商 ID:{{ supplierDetail.supplier_id }} +
+
+
+
基本信息
+

供应商名称:{{ 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 | 已重新核对 `开发细节`、现有代码与任务状态口径:确认主线未偏离订单全流程主题;同步修正文档中的两类偏差,一是将管理层端/司机端恢复为已接真实数据流的完成状态,二是将客户导入、供应商详情等仍含演示或最小样例实现的能力改回更保守表述。 |