diff --git a/docs/ops/2026-02-10__admin__order-module-repaired-full.md b/docs/ops/2026-02-10__admin__order-module-repaired-full.md new file mode 100644 index 00000000..05e1faaf --- /dev/null +++ b/docs/ops/2026-02-10__admin__order-module-repaired-full.md @@ -0,0 +1,52 @@ +# 订单模块全量修复与功能补全报告 (Stage A+B+C) + +## 摘要 +本次对 Admin 侧订单模块进行了深度的端到端修复,涵盖了核销记录、售后订单、收银订单、订单统计、订单配置及主列表。彻底解决了从 Mock 数据到真实数据库 RPC 接入的断层,并补齐了缺失的数据库字段。 + +## 修复范围 (Stage A+B+C) + +### Stage A: 核销记录模块 +- **UI 重构**:完全移除 `write-off-records/index.uvue` 的 Mock 数据。 +- **功能补全**:接入 `rpc_admin_write_off_record_list`,实现真实的分页、搜索及状态展示。 + +### Stage B: 售后/收银/配置模块 +- **售后订单**:修正了表头“实际支付”为“退款金额”的歧义,补齐了退款状态真实筛选逻辑。 +- **收银订单**:重构为调用 `orderService.fetchCashierOrderPage`,对齐支付与用户信息字段。 +- **订单配置**:将原本的 UI 模拟改为真实的 `get/save` 持久化逻辑,对接系统配置表。 + +### Stage C: 扩展字段与统计升级 +- **Schema 补全**:通过 `ml_orders_schema_update_v1.sql` 补齐了 `pay_type`(支付方式)和 `channel_type`(订单渠道)字段。 +- **RPC 升级**: + - `rpc_admin_order_list`:现在返回真实的支付和渠道信息。 + - `rpc_admin_order_source_stats`:从 `unknown` 汇总升级为真实的按渠道分组统计。 + - `rpc_admin_order_type_stats`:新增订单类型分析统计(普通/收银/核销)。 +- **页面对齐**:列表页和统计页现在展示真实的“微信支付”、“小程序”等业务标签。 + +## 变更清单 + +### 数据库/RPC (SQL) +- `docs/sql/10_schema/order/ml_orders_schema_update_v1.sql` (新增) +- `docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql` (升级) +- `docs/sql/30_rpc/order/rpc_admin_order_stats_v1.sql` (升级) +- `docs/sql/30_rpc/order/rpc_admin_order_trend_v1.sql` (升级) +- `docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql` (升级) +- `docs/sql/30_rpc/order/rpc_admin_order_type_stats_v1.sql` (新增) + +### 前端代码 +- `services/orderService.uts` (补全统计与列表方法) +- `pages/mall/admin/order/list.uvue` (逻辑与 UI 重构) +- `pages/mall/admin/order/order-statistics/index.uvue` (去 Mock 与 ECharts 动态驱动) +- `pages/mall/admin/order/aftersales-order/index.uvue` (标签修正与筛选逻辑) +- `pages/mall/admin/order/cashier-order/index.uvue` (RPC 接入与字段对齐) +- `pages/mall/admin/order/write-off-records/index.uvue` (完全重构接入) +- `pages/mall/admin/order/order-configuration/index.uvue` (配置持久化闭环) + +## 验证说明 +1. **数据库**:需依次执行 `10_schema` 下的补丁和 `30_rpc` 下的所有订单相关 SQL。 +2. **功能**: + - 订单管理列表:检查 Tabs 切换、搜索、分页是否联动后端。 + - 订单统计:观察趋势图和饼图是否不再是 30 天固定静态值。 + - 各子模块:确保进入页面后 Loading 结束能展示真实记录。 + +## 风险与后续 +- **支付方式映射**:当前 UI 按 1:余额, 2:微信, 3:支付宝, 4:线下 进行了硬编码映射,若后续支付方式变更,需同步更新 `list.uvue` 的映射函数。 diff --git a/docs/sql/10_schema/order/ml_orders_schema_update_v1.sql b/docs/sql/10_schema/order/ml_orders_schema_update_v1.sql index 21746f6d..fbe4d47f 100644 --- a/docs/sql/10_schema/order/ml_orders_schema_update_v1.sql +++ b/docs/sql/10_schema/order/ml_orders_schema_update_v1.sql @@ -26,4 +26,16 @@ BEGIN COMMENT ON COLUMN public.ml_orders.verifier_id IS '核销员ID'; END IF; + -- 4. 补齐 pay_type (支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付) + IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'pay_type') THEN + ALTER TABLE public.ml_orders ADD COLUMN pay_type INTEGER DEFAULT 1; + COMMENT ON COLUMN public.ml_orders.pay_type IS '支付方式: 1:余额, 2:微信, 3:支付宝, 4:线下支付'; + END IF; + + -- 5. 补齐 channel_type (订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP) + IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'ml_orders' AND column_name = 'channel_type') THEN + ALTER TABLE public.ml_orders ADD COLUMN channel_type INTEGER DEFAULT 1; + COMMENT ON COLUMN public.ml_orders.channel_type IS '订单渠道: 1:公众号, 2:小程序, 3:H5, 4:PC, 5:APP'; + END IF; + END $$; diff --git a/docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql b/docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql index aab2965f..d7f39d31 100644 --- a/docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql +++ b/docs/sql/30_rpc/order/rpc_admin_order_list_v1.sql @@ -59,6 +59,8 @@ BEGIN o.order_status, o.payment_status, o.shipping_status, + o.pay_type, + o.channel_type, o.paid_at, o.created_at, u.username as buyer_name, diff --git a/docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql b/docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql index c2cbb22e..6645528c 100644 --- a/docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql +++ b/docs/sql/30_rpc/order/rpc_admin_order_source_stats_v1.sql @@ -28,16 +28,26 @@ BEGIN RAISE EXCEPTION 'Permission denied'; END IF; - -- 2. 最小可用:按 unknown 聚合(排除已取消) + -- 2. 按渠道类型聚合统计(排除已取消) SELECT jsonb_agg(t) INTO v_items FROM ( SELECT - 'unknown'::text AS source, - COUNT(*) FILTER (WHERE o.order_status != 5) AS order_count, - COALESCE(SUM(o.total_amount) FILTER (WHERE o.order_status != 5), 0) AS total_amount + CASE o.channel_type + WHEN 1 THEN '公众号' + WHEN 2 THEN '小程序' + WHEN 3 THEN 'H5' + WHEN 4 THEN 'PC' + WHEN 5 THEN 'APP' + ELSE '其他' + END AS source, + COUNT(*) AS order_count, + COALESCE(SUM(o.total_amount), 0) AS total_amount FROM public.ml_orders o WHERE o.created_at >= p_start_time AND o.created_at <= p_end_time + AND o.order_status != 5 + GROUP BY o.channel_type + ORDER BY order_count DESC ) t; RETURN COALESCE(v_items, '[]'::jsonb); diff --git a/docs/sql/30_rpc/order/rpc_admin_order_type_stats_v1.sql b/docs/sql/30_rpc/order/rpc_admin_order_type_stats_v1.sql new file mode 100644 index 00000000..4db2011e --- /dev/null +++ b/docs/sql/30_rpc/order/rpc_admin_order_type_stats_v1.sql @@ -0,0 +1,60 @@ +-- ===================================================================================== +-- Admin 订单统计 - 订单类型分布统计 RPC +-- 位置:docs/sql/30_rpc/order/ +-- 对象类型:RPC 函数(SECURITY DEFINER) +-- 版本:v1 +-- 说明:按订单类型(普通、收银、核销)统计指定时间段内的销售额及其占比 +-- ===================================================================================== + +CREATE OR REPLACE FUNCTION public.rpc_admin_order_type_stats( + p_start_time TIMESTAMPTZ, + p_end_time TIMESTAMPTZ +) +RETURNS JSONB +SECURITY DEFINER +SET search_path = public +LANGUAGE plpgsql +AS $$ +DECLARE + v_total_amount DECIMAL(12,2); + v_items JSONB; +BEGIN + -- 1. 权限检查 + IF NOT EXISTS ( + SELECT 1 FROM public.ak_users + WHERE auth_id = auth.uid() AND role IN ('admin', 'analytics') + ) THEN + RAISE EXCEPTION 'Permission denied'; + END IF; + + -- 2. 计算总销售额(用于算占比) + SELECT COALESCE(SUM(total_amount), 0) INTO v_total_amount + FROM public.ml_orders + WHERE created_at >= p_start_time AND created_at <= p_end_time + AND order_status != 5; -- 排除已取消 + + -- 3. 按类型统计 + SELECT jsonb_agg(t) INTO v_items + FROM ( + SELECT + CASE o.order_type + WHEN 1 THEN '普通订单' + WHEN 2 THEN '收银订单' + WHEN 3 THEN '核销订单' + ELSE '其他类型' + END AS name, + COALESCE(SUM(o.total_amount), 0) AS amount, + CASE + WHEN v_total_amount > 0 THEN ROUND((COALESCE(SUM(o.total_amount), 0) / v_total_amount * 100), 2) + ELSE 0 + END AS rate + FROM public.ml_orders o + WHERE o.created_at >= p_start_time AND o.created_at <= p_end_time + AND o.order_status != 5 + GROUP BY o.order_type + ORDER BY amount DESC + ) t; + + RETURN COALESCE(v_items, '[]'::jsonb); +END; +$$; diff --git a/pages/mall/admin/order/aftersales-order/index.uvue b/pages/mall/admin/order/aftersales-order/index.uvue index 1d2bdbeb..5b1afe87 100644 --- a/pages/mall/admin/order/aftersales-order/index.uvue +++ b/pages/mall/admin/order/aftersales-order/index.uvue @@ -5,10 +5,12 @@ 退款状态: - - 全部 - - + + + {{ statusOptions[statusIndex].label }} + + + @@ -39,7 +41,7 @@ 原订单号 商品信息 用户信息 - 实际支付 + 退款金额 发起退款时间 退款状态 订单状态 @@ -132,6 +134,20 @@ const totalPages = computed((): number => { }) const refundStatusFilter = ref(null) +const statusIndex = ref(0) +const statusOptions = [ + { label: '全部', value: null as number | null }, + { label: '申请中', value: 1 as number | null }, + { label: '已同意', value: 2 as number | null }, + { label: '已拒绝', value: 3 as number | null }, + { label: '已完成', value: 4 as number | null } +] + +const onStatusChange = (e : any) => { + statusIndex.value = e.detail.value as number + refundStatusFilter.value = statusOptions[statusIndex.value].value + handleQuery() +} const loadRefundOrders = async () => { loading.value = true diff --git a/pages/mall/admin/order/cashier-order/index.uvue b/pages/mall/admin/order/cashier-order/index.uvue index 16d16b36..5b65cecb 100644 --- a/pages/mall/admin/order/cashier-order/index.uvue +++ b/pages/mall/admin/order/cashier-order/index.uvue @@ -15,12 +15,12 @@ 订单号: - + 用户名: - + @@ -47,11 +47,17 @@ - + + 加载中... + + + 暂无收银订单 + + {{ item.orderId }} {{ item.userInfo }} - {{ item.payPrice.toFixed(2) }} - {{ item.discountPrice.toFixed(2) }} + ¥{{ item.payPrice.toFixed(2) }} + ¥{{ item.discountPrice.toFixed(2) }} {{ item.payTime }} @@ -62,17 +68,20 @@ 共 {{ total }} 条 - - 15条/页 ▼ - - < - {{ page }} - > + + < + + + {{ page }} + + + > + 前往 - + @@ -120,15 +129,13 @@ const username = ref('') const total = ref(0) const orderList = ref([]) const loading = ref(false) - const page = ref(1) const pageSize = ref(15) const jumpPage = ref('') const totalPages = computed((): number => { if (pageSize.value <= 0) return 1 - const pages = Math.ceil(total.value / pageSize.value) - return pages <= 0 ? 1 : pages + return Math.ceil(total.value / pageSize.value) }) const loadCashierOrders = async () => { @@ -143,17 +150,16 @@ const loadCashierOrders = async () => { orderList.value = res.items.map((item: any): CashierOrder => { return { - orderId: String(item.order_no ?? '--'), + orderId: String(item.order_no), userInfo: `${String(item.customer_name ?? '未知')} | ${String(item.customer_phone ?? '')}`, - payPrice: parseFloat(String(item.total_amount ?? item.pay_amount ?? '0')), + payPrice: parseFloat(String(item.total_amount ?? '0')), discountPrice: parseFloat(String(item.discount_amount ?? '0')), payTime: String(item.paid_at ?? '--') } as CashierOrder }) - total.value = res.total } catch (e) { - uni.showToast({ title: '收银订单加载失败', icon: 'none' }) + uni.showToast({ title: '加载收银订单失败', icon: 'none' }) } finally { loading.value = false } @@ -189,7 +195,7 @@ const goToJumpPage = () => { loadCashierOrders() jumpPage.value = '' } else { - uni.showToast({ title: '请输入有效的页码', icon: 'none' }) + uni.showToast({ title: '页码无效', icon: 'none' }) } } @@ -345,9 +351,8 @@ const closeQrModal = () => { gap: 12px; } .total-txt { font-size: 14px; color: #606266; } -.page-val { font-size: 14px; color: #606266; border: 1px solid #dcdfe6; padding: 4px 10px; border-radius: 4px; } .page-btns { display: flex; flex-direction: row; gap: 8px; } -.p-btn { +.page-btn { width: 32px; height: 32px; border: 1px solid #dcdfe6; @@ -356,9 +361,10 @@ const closeQrModal = () => { align-items: center; justify-content: center; font-size: 14px; + cursor: pointer; } -.p-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; } -.p-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; } +.page-btn.active { background-color: #2d8cf0; border-color: #2d8cf0; color: #fff; } +.page-btn.disabled { color: #c0c4cc; background-color: #f5f7fa; cursor: not-allowed; } .page-jump { display: flex; flex-direction: row; align-items: center; gap: 8px; } .jump-txt { font-size: 14px; color: #606266; } @@ -453,5 +459,3 @@ const closeQrModal = () => { to { background-color: rgba(0, 0, 0, 0); } } - - diff --git a/pages/mall/admin/order/list.uvue b/pages/mall/admin/order/list.uvue index 5c1f1cb2..ecce4d51 100644 --- a/pages/mall/admin/order/list.uvue +++ b/pages/mall/admin/order/list.uvue @@ -94,7 +94,7 @@ {{ item.order_no }} - [普通订单] + [{{ getChannelName(item.channel_type) }}] @@ -113,7 +113,7 @@ - 余额支付 + {{ getPayTypeName(item.pay_type) }} @@ -252,6 +252,27 @@ function getStatusName(status: number): string { const tab = statusTabs.find(t => t.value === status) return tab?.name ?? '未知' } + +function getPayTypeName(type: number | null): string { + switch (type) { + case 1: return '余额支付' + case 2: return '微信支付' + case 3: return '支付宝' + case 4: return '线下支付' + default: return '其他' + } +} + +function getChannelName(type: number | null): string { + switch (type) { + case 1: return '公众号' + case 2: return '小程序' + case 3: return 'H5' + case 4: return 'PC' + case 5: return 'APP' + default: return '普通订单' + } +} - - diff --git a/services/orderService.uts b/services/orderService.uts index 06d92892..d779c024 100644 --- a/services/orderService.uts +++ b/services/orderService.uts @@ -121,6 +121,13 @@ export async function fetchOrderSourceStats(startTime: string, endTime: string): } as any)) as any } +export async function fetchOrderTypeStats(startTime: string, endTime: string): Promise> { + return (await rpcOrEmptyArray('rpc_admin_order_type_stats', { + p_start_time: startTime, + p_end_time: endTime + } as any)) as any +} + export async function getOrderSettings(): Promise { const res = await rpcOrValue('rpc_admin_system_config_get', { p_key: 'order_settings'