提交
This commit is contained in:
1 parent
493f8d6756
commit
d84f4b944c
406 files changed
+58954
-10376
No files matched your search
@@ -1,20 +0,0 @@
|
||||
-- 移除地磅「车牌管理」独立主数据表及菜单(车牌统一使用运输管理 pl_vehicle)
|
||||
DROP TABLE IF EXISTS `db_vehicle`;
|
||||
|
||||
DELETE FROM `sys_role_menu`
|
||||
WHERE `menu_id` IN (
|
||||
'DB00000000000000000000000000006',
|
||||
'DB10000000000000000000000000014',
|
||||
'DB10000000000000000000000000015',
|
||||
'DB10000000000000000000000000016',
|
||||
'DB10000000000000000000000000017'
|
||||
);
|
||||
|
||||
DELETE FROM `sys_menu`
|
||||
WHERE `id` IN (
|
||||
'DB00000000000000000000000000006',
|
||||
'DB10000000000000000000000000014',
|
||||
'DB10000000000000000000000000015',
|
||||
'DB10000000000000000000000000016',
|
||||
'DB10000000000000000000000000017'
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
-- 出入库单 · 工厂现场人员免登录链接
|
||||
-- Spec: docs/superpowers/specs/2026-06-23-pl-site-inventory-link-design.md
|
||||
|
||||
ALTER TABLE db_inbound_outbound
|
||||
ADD COLUMN site_access_token VARCHAR(64) NULL COMMENT '现场人员免登录访问令牌' AFTER remark,
|
||||
ADD COLUMN site_feedback_remark VARCHAR(500) NULL COMMENT '现场回执备注' AFTER site_access_token,
|
||||
ADD COLUMN site_feedback_images TEXT NULL COMMENT '现场回执附件URLs(JSON数组)' AFTER site_feedback_remark,
|
||||
ADD COLUMN site_feedback_time DATETIME NULL COMMENT '现场回执保存时间' AFTER site_feedback_images;
|
||||
|
||||
CREATE UNIQUE INDEX uk_db_inbound_outbound_site_token
|
||||
ON db_inbound_outbound(site_access_token);
|
||||
@@ -1,35 +0,0 @@
|
||||
-- 运输计划 · 工厂现场链接(与地磅出入库单解耦)
|
||||
-- 入库/出库计划安排运输时生成,不在 db_inbound_outbound 落库
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pl_transport_site_link (
|
||||
id VARCHAR(64) NOT NULL COMMENT '主键ID',
|
||||
transport_id VARCHAR(64) NOT NULL COMMENT '运输单ID',
|
||||
plan_id VARCHAR(64) NOT NULL COMMENT '运输计划ID',
|
||||
plan_product_id VARCHAR(64) NOT NULL COMMENT '计划产品行ID',
|
||||
site_access_token VARCHAR(64) NOT NULL COMMENT '现场人员免登录访问令牌',
|
||||
business_type VARCHAR(20) NOT NULL COMMENT '业务类型:入库/出库',
|
||||
partner_company VARCHAR(200) NULL COMMENT '往来单位',
|
||||
product_id VARCHAR(64) NULL COMMENT 'CRM产品ID',
|
||||
product VARCHAR(200) NULL COMMENT '产品名称快照',
|
||||
specification VARCHAR(200) NULL COMMENT '规格快照',
|
||||
quantity DECIMAL(15, 3) NULL COMMENT '数量(吨)',
|
||||
vehicle_no VARCHAR(32) NULL COMMENT '车牌号',
|
||||
plan_no VARCHAR(64) NULL COMMENT '运输计划号',
|
||||
plan_date DATE NULL COMMENT '计划日期',
|
||||
site_feedback_remark VARCHAR(500) NULL COMMENT '现场回执备注',
|
||||
site_feedback_images TEXT NULL COMMENT '现场回执附件URLs(JSON数组)',
|
||||
site_feedback_time DATETIME NULL COMMENT '现场回执保存时间',
|
||||
tenant_no VARCHAR(64) NOT NULL COMMENT '租户编号',
|
||||
order_num INT NULL COMMENT '排序号',
|
||||
remark VARCHAR(512) NULL COMMENT '备注',
|
||||
create_by VARCHAR(64) NULL COMMENT '创建人id',
|
||||
create_dept_id VARCHAR(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
update_by VARCHAR(64) NULL COMMENT '更新人id',
|
||||
update_time DATETIME NOT NULL COMMENT '更新时间',
|
||||
del_flag TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pl_transport_site_link_token (site_access_token),
|
||||
KEY idx_pl_transport_site_link_transport (transport_id, del_flag),
|
||||
KEY idx_pl_transport_site_link_plan (plan_id, del_flag)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运输计划现场链接';
|
||||
@@ -1,135 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 运费结算完善 · 审批表 + 字典 + 按钮权限
|
||||
-- 执行库:shanxia2.0 门户库
|
||||
-- 说明:可重复执行
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. 审批记录表
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pl_freight_settlement_approval` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`settlement_id` varchar(64) NOT NULL COMMENT '结算单ID',
|
||||
`step` int(11) NOT NULL DEFAULT 1 COMMENT '步骤序号',
|
||||
`step_name` varchar(50) DEFAULT NULL COMMENT '步骤名称',
|
||||
`approver_id` varchar(64) NOT NULL COMMENT '审批人ID',
|
||||
`approver_name` varchar(64) DEFAULT NULL COMMENT '审批人姓名',
|
||||
`approval_type` varchar(20) NOT NULL DEFAULT 'sequential' COMMENT 'sequential/parallel/or',
|
||||
`approval_result` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/skipped',
|
||||
`approval_opinion` varchar(500) DEFAULT NULL COMMENT '审批意见',
|
||||
`approval_time` datetime DEFAULT NULL COMMENT '审批时间',
|
||||
`is_current` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否当前节点',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_pl_fsa_settlement` (`settlement_id`, `del_flag`),
|
||||
KEY `idx_pl_fsa_approver` (`approver_id`, `approval_result`, `is_current`, `del_flag`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运费结算审批记录';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. 结算状态字典
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_dict_data` (
|
||||
`id`, `dict_label`, `dict_value`, `tag_type`, `dict_type`, `tenant_no`, `order_num`,
|
||||
`remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
) VALUES
|
||||
(
|
||||
'PL_SETTLEMENT_STATUS_PENDING', '草稿', 'pending', 'info', 'pl_settlement_status', 'shanxia2.0', 1,
|
||||
NULL, 'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_SETTLE_APPROVING', '审批中', 'approving', 'warning', 'pl_settlement_status', 'shanxia2.0', 2,
|
||||
NULL, 'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_SETTLE_APPROVED', '待发放', 'approved', 'primary', 'pl_settlement_status', 'shanxia2.0', 3,
|
||||
NULL, 'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_SETTLEMENT_STATUS_CONFIRMED', '已结算', 'confirmed', 'success', 'pl_settlement_status', 'shanxia2.0', 4,
|
||||
NULL, 'system', '', NOW(), NULL, NOW(), 0
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`dict_label` = VALUES(`dict_label`),
|
||||
`tag_type` = VALUES(`tag_type`),
|
||||
`order_num` = VALUES(`order_num`),
|
||||
`update_time` = NOW(),
|
||||
`del_flag` = 0;
|
||||
|
||||
UPDATE `sys_dict_data`
|
||||
SET `del_flag` = 1, `update_time` = NOW()
|
||||
WHERE `dict_type` = 'pl_settlement_status'
|
||||
AND `dict_value` IN ('submitted')
|
||||
AND `del_flag` = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. 运费结算按钮权限(parent PL_FREIGHT)
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_menu` (
|
||||
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
|
||||
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
) VALUES
|
||||
('PL_FREIGHT_Q', '查询', 'PL_FREIGHT', 'F', '', '', 'pl:freight:query', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_C', '组单', 'PL_FREIGHT', 'F', '', '', 'pl:freight:create', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_E', '编辑', 'PL_FREIGHT', 'F', '', '', 'pl:freight:edit', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_S', '提交审批', 'PL_FREIGHT', 'F', '', '', 'pl:freight:submit', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_A', '审批', 'PL_FREIGHT', 'F', '', '', 'pl:freight:approve', 1, '1', '1', 'shanxia2.0', 5, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_W', '撤回', 'PL_FREIGHT', 'F', '', '', 'pl:freight:withdraw', 1, '1', '1', 'shanxia2.0', 6, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_FREIGHT_D', '发放登记', 'PL_FREIGHT', 'F', '', '', 'pl:freight:disburse', 1, '1', '1', 'shanxia2.0', 7, NULL, 'system', '', NOW(), NULL, NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`perms` = VALUES(`perms`),
|
||||
`update_time` = NOW(),
|
||||
`del_flag` = 0;
|
||||
|
||||
-- 可选:为已有运费结算菜单的角色自动授权按钮
|
||||
INSERT INTO `sys_role_menu` (
|
||||
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
rm.`role_id`,
|
||||
m.`menu_id`,
|
||||
rm.`tenant_no`,
|
||||
m.`order_num`,
|
||||
'pl freight buttons auto grant',
|
||||
'system',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM `sys_role_menu` rm
|
||||
INNER JOIN (
|
||||
SELECT 'PL_FREIGHT_Q' AS menu_id, 1 AS order_num UNION ALL
|
||||
SELECT 'PL_FREIGHT_C', 2 UNION ALL
|
||||
SELECT 'PL_FREIGHT_E', 3 UNION ALL
|
||||
SELECT 'PL_FREIGHT_S', 4 UNION ALL
|
||||
SELECT 'PL_FREIGHT_A', 5 UNION ALL
|
||||
SELECT 'PL_FREIGHT_W', 6 UNION ALL
|
||||
SELECT 'PL_FREIGHT_D', 7
|
||||
) m ON 1 = 1
|
||||
WHERE rm.`menu_id` = 'PL_FREIGHT'
|
||||
AND rm.`del_flag` = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_menu` x
|
||||
WHERE x.`role_id` = rm.`role_id` AND x.`menu_id` = m.`menu_id` AND x.`del_flag` = 0
|
||||
);
|
||||
|
||||
SELECT `id`, `menu_name`, `perms` FROM `sys_menu`
|
||||
WHERE `id` LIKE 'PL_FREIGHT%' AND `del_flag` = 0 ORDER BY `order_num`;
|
||||
@@ -1,114 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 运输计划归档 · 菜单与按钮权限
|
||||
-- 执行库:shanxia2.0 门户库(与 web-admin 同库)
|
||||
-- 说明:可重复执行(ON DUPLICATE KEY UPDATE / NOT EXISTS)
|
||||
-- 执行后:重新登录或刷新菜单缓存,侧栏「运输管理」下出现「运输计划归档」
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. 菜单(C)+ 按钮权限(F)
|
||||
-- 路由:/#/pl/plan-archive
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_menu` (
|
||||
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
|
||||
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
) VALUES
|
||||
(
|
||||
'PL_PLAN_ARCHIVE', '运输计划归档', 'PL_MENU_ROOT', 'C',
|
||||
'plan-archive', 'pl/plan-archive/index', 'pl:plan-archive:list',
|
||||
1, '1', '1', 'shanxia2.0', 11, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_PLAN_ARCHIVE_Q', '查询', 'PL_PLAN_ARCHIVE', 'F',
|
||||
'', '', 'pl:plan-archive:query',
|
||||
1, '1', '1', 'shanxia2.0', 1, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_PLAN_ARCHIVE_E', '编辑', 'PL_PLAN_ARCHIVE', 'F',
|
||||
'', '', 'pl:plan-archive:edit',
|
||||
1, '1', '1', 'shanxia2.0', 2, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_PLAN_ARCHIVE_A', '归档', 'PL_PLAN_ARCHIVE', 'F',
|
||||
'', '', 'pl:plan-archive:archive',
|
||||
1, '1', '1', 'shanxia2.0', 3, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`path` = VALUES(`path`),
|
||||
`component` = VALUES(`component`),
|
||||
`perms` = VALUES(`perms`),
|
||||
`visible` = VALUES(`visible`),
|
||||
`status` = VALUES(`status`),
|
||||
`order_num` = VALUES(`order_num`),
|
||||
`update_time` = NOW(),
|
||||
`del_flag` = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. 调整「危化品证上传」排序,排在归档菜单之后
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
UPDATE `sys_menu`
|
||||
SET `order_num` = 12, `update_time` = NOW()
|
||||
WHERE `id` = 'PL_PLAN_CERT'
|
||||
AND `del_flag` = 0
|
||||
AND (`order_num` IS NULL OR `order_num` <> 12);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3.(可选)角色授权:为已拥有「运输计划」菜单的角色,自动勾选归档菜单及按钮
|
||||
-- 若需手工在「系统管理 → 角色管理」里分配,可注释掉本节
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_role_menu` (
|
||||
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
rm.`role_id`,
|
||||
m.`menu_id`,
|
||||
rm.`tenant_no`,
|
||||
m.`order_num`,
|
||||
'pl plan archive auto grant',
|
||||
'system',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM `sys_role_menu` rm
|
||||
INNER JOIN (
|
||||
SELECT 'PL_PLAN_ARCHIVE' AS menu_id, 11 AS order_num UNION ALL
|
||||
SELECT 'PL_PLAN_ARCHIVE_Q', 1 UNION ALL
|
||||
SELECT 'PL_PLAN_ARCHIVE_E', 2 UNION ALL
|
||||
SELECT 'PL_PLAN_ARCHIVE_A', 3
|
||||
) m ON 1 = 1
|
||||
WHERE rm.`menu_id` = 'PL_PLAN'
|
||||
AND rm.`del_flag` = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `sys_role_menu` x
|
||||
WHERE x.`role_id` = rm.`role_id`
|
||||
AND x.`menu_id` = m.`menu_id`
|
||||
AND x.`del_flag` = 0
|
||||
);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. 校验(执行后可查看结果)
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
SELECT `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `order_num`
|
||||
FROM `sys_menu`
|
||||
WHERE `id` IN ('PL_PLAN_ARCHIVE', 'PL_PLAN_ARCHIVE_Q', 'PL_PLAN_ARCHIVE_E', 'PL_PLAN_ARCHIVE_A')
|
||||
AND `del_flag` = 0
|
||||
ORDER BY `id`;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- 运费结算:发票附件字段
|
||||
ALTER TABLE `pl_freight_settlement`
|
||||
ADD COLUMN `invoice_attachment_urls` text DEFAULT NULL COMMENT '发票附件URLs,逗号分隔' AFTER `invoice_date`;
|
||||
@@ -1,99 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 运费结算 · 拆分为三菜单(结算 / 审批 / 财务发放)
|
||||
-- 执行库:shanxia2.0 门户库
|
||||
-- 说明:可重复执行;执行后重新登录刷新菜单
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. 新增菜单
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_menu` (
|
||||
`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
|
||||
`is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
) VALUES
|
||||
(
|
||||
'PL_FREIGHT_APPROVAL', '运费结算审批', 'PL_MENU_ROOT', 'C',
|
||||
'freight-approval', 'pl/freight-approval/index', 'pl:freight-approval:list',
|
||||
1, '1', '1', 'shanxia2.0', 31, '待审批与已审批结算单',
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_FREIGHT_APPROVAL_Q', '查询', 'PL_FREIGHT_APPROVAL', 'F',
|
||||
'', '', 'pl:freight-approval:query',
|
||||
1, '1', '1', 'shanxia2.0', 1, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_FREIGHT_DISBURSE_MENU', '运费财务发放', 'PL_MENU_ROOT', 'C',
|
||||
'freight-disburse', 'pl/freight-disburse/index', 'pl:freight-disburse:list',
|
||||
1, '1', '1', 'shanxia2.0', 32, '待发放与已发放结算单',
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
),
|
||||
(
|
||||
'PL_FREIGHT_DISBURSE_Q', '查询', 'PL_FREIGHT_DISBURSE_MENU', 'F',
|
||||
'', '', 'pl:freight-disburse:query',
|
||||
1, '1', '1', 'shanxia2.0', 1, NULL,
|
||||
'system', '', NOW(), NULL, NOW(), 0
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`menu_name` = VALUES(`menu_name`),
|
||||
`parent_id` = VALUES(`parent_id`),
|
||||
`menu_type` = VALUES(`menu_type`),
|
||||
`path` = VALUES(`path`),
|
||||
`component` = VALUES(`component`),
|
||||
`perms` = VALUES(`perms`),
|
||||
`visible` = VALUES(`visible`),
|
||||
`status` = VALUES(`status`),
|
||||
`order_num` = VALUES(`order_num`),
|
||||
`update_time` = NOW(),
|
||||
`del_flag` = 0;
|
||||
|
||||
-- 运费结算菜单排序保持在审批、发放之前
|
||||
UPDATE `sys_menu`
|
||||
SET `order_num` = 30, `update_time` = NOW()
|
||||
WHERE `id` = 'PL_FREIGHT' AND `del_flag` = 0;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. 为已拥有「运费结算」菜单的角色自动授权新菜单
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO `sys_role_menu` (
|
||||
`id`, `role_id`, `menu_id`, `tenant_no`, `order_num`, `remark`,
|
||||
`create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
|
||||
)
|
||||
SELECT
|
||||
REPLACE(UUID(), '-', ''),
|
||||
rm.`role_id`,
|
||||
m.`menu_id`,
|
||||
rm.`tenant_no`,
|
||||
m.`order_num`,
|
||||
'pl freight split menus auto grant',
|
||||
'system',
|
||||
'',
|
||||
NOW(),
|
||||
NULL,
|
||||
NOW(),
|
||||
0
|
||||
FROM `sys_role_menu` rm
|
||||
INNER JOIN (
|
||||
SELECT 'PL_FREIGHT_APPROVAL' AS menu_id, 31 AS order_num UNION ALL
|
||||
SELECT 'PL_FREIGHT_APPROVAL_Q', 1 UNION ALL
|
||||
SELECT 'PL_FREIGHT_DISBURSE_MENU', 32 UNION ALL
|
||||
SELECT 'PL_FREIGHT_DISBURSE_Q', 1
|
||||
) m ON 1 = 1
|
||||
WHERE rm.`menu_id` = 'PL_FREIGHT'
|
||||
AND rm.`del_flag` = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM `sys_role_menu` x
|
||||
WHERE x.`role_id` = rm.`role_id` AND x.`menu_id` = m.`menu_id` AND x.`del_flag` = 0
|
||||
);
|
||||
|
||||
SELECT `id`, `menu_name`, `path`, `component`, `perms`, `order_num`
|
||||
FROM `sys_menu`
|
||||
WHERE `id` IN ('PL_FREIGHT', 'PL_FREIGHT_APPROVAL', 'PL_FREIGHT_DISBURSE_MENU')
|
||||
AND `del_flag` = 0
|
||||
ORDER BY `order_num`;
|
||||
@@ -1,26 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 运输管理 · 调整「山峡自有车辆」与「车辆管理」菜单排序
|
||||
-- 执行库:shanxia2.0 门户库(与 web-admin 同库)
|
||||
-- 说明:可重复执行;交换 order_num,使「山峡自有车辆」排在「车辆管理」之上
|
||||
-- 执行后:重新登录或刷新菜单缓存
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE `sys_menu`
|
||||
SET `order_num` = 2, `update_time` = NOW()
|
||||
WHERE `id` = 'PL_SELF_VEHICLE'
|
||||
AND `del_flag` = 0
|
||||
AND (`order_num` IS NULL OR `order_num` <> 2);
|
||||
|
||||
UPDATE `sys_menu`
|
||||
SET `order_num` = 4, `update_time` = NOW()
|
||||
WHERE `id` = 'PL_VEHICLE'
|
||||
AND `del_flag` = 0
|
||||
AND (`order_num` IS NULL OR `order_num` <> 4);
|
||||
|
||||
SELECT `id`, `menu_name`, `parent_id`, `order_num`
|
||||
FROM `sys_menu`
|
||||
WHERE `id` IN ('PL_SELF_VEHICLE', 'PL_VEHICLE')
|
||||
AND `del_flag` = 0
|
||||
ORDER BY `order_num`;
|
||||
@@ -1,32 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- 运输管理 · 车辆表增加挂车号
|
||||
-- 执行库:shanxia2.0 门户库(与 web-admin 同库)
|
||||
-- 说明:可重复执行;挂车号为车辆主数据属性,地磅/出入库仍使用 license_plate
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
SET @col_exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pl_vehicle'
|
||||
AND COLUMN_NAME = 'trailer_no'
|
||||
);
|
||||
|
||||
SET @ddl := IF(
|
||||
@col_exists = 0,
|
||||
'ALTER TABLE `pl_vehicle` ADD COLUMN `trailer_no` varchar(20) DEFAULT NULL COMMENT ''挂车号'' AFTER `license_plate`',
|
||||
'SELECT ''trailer_no already exists'' AS message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'pl_vehicle'
|
||||
AND COLUMN_NAME IN ('license_plate', 'trailer_no')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
+201
-1
@@ -21,6 +21,14 @@ CREATE TABLE `pl_carrier` (
|
||||
`contact_name` varchar(100) DEFAULT NULL COMMENT '联系人',
|
||||
`contact_phone` varchar(32) DEFAULT NULL COMMENT '联系电话',
|
||||
`status` char(1) NOT NULL DEFAULT '1' COMMENT '合作状态 1-合作中 0-已终止',
|
||||
`business_license_urls` varchar(2000) DEFAULT NULL COMMENT '营业执照附件URL,逗号分隔',
|
||||
`road_transport_license_urls` varchar(2000) DEFAULT NULL COMMENT '道路经营许可证附件URL,逗号分隔',
|
||||
`transport_agreement_urls` varchar(2000) DEFAULT NULL COMMENT '运输协议附件URL,逗号分隔',
|
||||
`transport_agreement_expire_date` date DEFAULT NULL COMMENT '运输协议有效期',
|
||||
`audit_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '审核状态 pending-待审核 approved-已通过 rejected-已驳回',
|
||||
`audit_by` varchar(64) DEFAULT NULL COMMENT '审核人id',
|
||||
`audit_time` datetime DEFAULT NULL COMMENT '审核时间',
|
||||
`audit_remark` varchar(512) DEFAULT NULL COMMENT '审核备注(驳回原因等)',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
@@ -97,6 +105,7 @@ CREATE TABLE `pl_cert` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`owner_type` varchar(20) NOT NULL COMMENT '归属类型 vehicle-车辆 personnel-人员',
|
||||
`owner_id` varchar(64) NOT NULL COMMENT '归属主体ID',
|
||||
`vehicle_part` varchar(20) DEFAULT NULL COMMENT '车辆部位 head-主车 trailer-挂车,人员证书为空',
|
||||
`cert_name` varchar(100) NOT NULL COMMENT '证书名称',
|
||||
`cert_no` varchar(64) DEFAULT NULL COMMENT '证号',
|
||||
`expire_date` date DEFAULT NULL COMMENT '有效期',
|
||||
@@ -643,9 +652,200 @@ CREATE TABLE `pl_document_reminder` (
|
||||
KEY `idx_tenant` (`tenant_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='证件到期提醒表';
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_recharge_apply (车辆ETC/加油充值申请)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_recharge_apply`;
|
||||
CREATE TABLE `pl_vehicle_recharge_apply` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`apply_no` varchar(32) NOT NULL COMMENT '申请单号 SXSQ+YYYYMMDD+6位',
|
||||
`license_plate` varchar(20) NOT NULL COMMENT '车牌号',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`apply_date` date NOT NULL COMMENT '申请日期',
|
||||
`odometer_km` decimal(10,1) DEFAULT NULL COMMENT '期初公里数',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`applicant_id` varchar(64) NOT NULL COMMENT '申请人用户ID',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending-待发放 disbursed-已发放 cancelled-已作废',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_apply_no` (`apply_no`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_license_plate` (`license_plate`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_date` (`apply_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值申请';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_recharge_record (车辆ETC/加油充值发放登记)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_recharge_record`;
|
||||
CREATE TABLE `pl_vehicle_recharge_record` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`recharge_order_no` varchar(32) NOT NULL COMMENT '充值订单号 SXCL+YYYYMMDD+6位',
|
||||
`recharge_date` date NOT NULL COMMENT '充值日期',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`card_no` varchar(64) DEFAULT NULL COMMENT '卡号',
|
||||
`gas_station` varchar(200) DEFAULT NULL COMMENT '加油站/充值渠道',
|
||||
`amount` decimal(10,2) NOT NULL COMMENT '充值金额(元)',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`apply_id` varchar(64) DEFAULT NULL COMMENT '关联申请单ID',
|
||||
`license_plate` varchar(20) DEFAULT NULL COMMENT '车牌号冗余',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_recharge_order_no` (`recharge_order_no`),
|
||||
UNIQUE KEY `uk_apply_id` (`apply_id`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_recharge_date` (`recharge_date`),
|
||||
KEY `idx_license_plate` (`license_plate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值发放登记';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_maintenance_apply (车辆维保申请)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_maintenance_apply`;
|
||||
CREATE TABLE `pl_vehicle_maintenance_apply` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`apply_no` varchar(32) NOT NULL COMMENT '申请单号 SXWB+YYYYMMDD+6位',
|
||||
`license_plate` varchar(20) NOT NULL COMMENT '车牌号',
|
||||
`applicant_id` varchar(64) NOT NULL COMMENT '申请人用户ID',
|
||||
`apply_date` date NOT NULL COMMENT '申请时间',
|
||||
`maintenance_type` varchar(64) NOT NULL COMMENT '维保类型',
|
||||
`start_date` date NOT NULL COMMENT '维保开始时间',
|
||||
`expected_pickup_date` date DEFAULT NULL COMMENT '预计取车时间',
|
||||
`maintenance_content` varchar(1000) DEFAULT NULL COMMENT '维保内容',
|
||||
`shop_address` varchar(500) DEFAULT NULL COMMENT '维保店铺地址',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending-待处理 processed-已处理 cancelled-已作废',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_apply_no` (`apply_no`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_license_plate` (`license_plate`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_date` (`apply_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆维保申请';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_maintenance_process (车辆维保处理登记)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_maintenance_process`;
|
||||
CREATE TABLE `pl_vehicle_maintenance_process` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`process_no` varchar(32) NOT NULL COMMENT '处理单号 SXWC+YYYYMMDD+6位',
|
||||
`apply_id` varchar(64) DEFAULT NULL COMMENT '关联申请单ID',
|
||||
`license_plate` varchar(20) DEFAULT NULL COMMENT '车牌号冗余',
|
||||
`amount` decimal(10,2) DEFAULT NULL COMMENT '金额(元)',
|
||||
`handler_id` varchar(64) NOT NULL COMMENT '处理人用户ID',
|
||||
`process_date` date NOT NULL COMMENT '处理时间',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_process_no` (`process_no`),
|
||||
UNIQUE KEY `uk_apply_id` (`apply_id`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_process_date` (`process_date`),
|
||||
KEY `idx_license_plate` (`license_plate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆维保处理登记';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_urea_apply (车辆尿素申请)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_urea_apply`;
|
||||
CREATE TABLE `pl_vehicle_urea_apply` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`apply_no` varchar(32) NOT NULL COMMENT '申请单号 SXNS+YYYYMMDD+6位',
|
||||
`license_plate` varchar(20) NOT NULL COMMENT '车牌号',
|
||||
`applicant_id` varchar(64) NOT NULL COMMENT '申请人用户ID',
|
||||
`apply_date` date NOT NULL COMMENT '申请时间',
|
||||
`warehouse_in_date` date NOT NULL COMMENT '入库时间',
|
||||
`unit` varchar(32) NOT NULL COMMENT '单位',
|
||||
`quantity` decimal(10,2) NOT NULL COMMENT '入库数',
|
||||
`apply_amount` decimal(10,2) NOT NULL COMMENT '申请金额(元)',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending-待发放 disbursed-已发放 cancelled-已作废',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_apply_no` (`apply_no`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_license_plate` (`license_plate`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_date` (`apply_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆尿素申请';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for pl_vehicle_urea_disburse (车辆尿素发放登记)
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `pl_vehicle_urea_disburse`;
|
||||
CREATE TABLE `pl_vehicle_urea_disburse` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`disburse_no` varchar(32) NOT NULL COMMENT '发放单号 SXUF+YYYYMMDD+6位',
|
||||
`apply_id` varchar(64) DEFAULT NULL COMMENT '关联申请单ID',
|
||||
`license_plate` varchar(20) DEFAULT NULL COMMENT '车牌号冗余',
|
||||
`amount` decimal(10,2) NOT NULL COMMENT '发放金额(元)',
|
||||
`handler_id` varchar(64) NOT NULL COMMENT '处理人用户ID',
|
||||
`disburse_date` date NOT NULL COMMENT '发放时间',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_disburse_no` (`disburse_no`),
|
||||
UNIQUE KEY `uk_apply_id` (`apply_id`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_disburse_date` (`disburse_date`),
|
||||
KEY `idx_license_plate` (`license_plate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆尿素发放登记';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- =============================================================================
|
||||
-- DDL 完成
|
||||
-- 共计 16 张表(含 pl_cert;pl_driver 已由 pl_personnel 替代)
|
||||
-- 共计 22 张表(含 pl_cert;pl_driver 已由 pl_personnel 替代)
|
||||
-- =============================================================================
|
||||
+749
-50
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,264 @@
|
||||
# 仅创建人可删除 — 删除接口 / 页面对照表与分阶段改造方案
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 业务数据删除时,仅允许 `create_by === 当前用户`(`super_admin` 可例外);后端强制校验,前端按创建人显隐/禁用删除按钮。
|
||||
|
||||
**Architecture:** 后端新增统一 `assert_row_deletable_by_creator()`,在各模块 `soft_delete` / `delete_*` 入口调用;前端新增 `canDeleteByCreator()` 工具与可选 `CreatorDeleteButton` 包装;列表批量删除过滤非本人数据并提示。系统管理类主数据默认**不纳入**本规则(见 §4)。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy(backend)、Vue 3 + Element Plus(web-admin)
|
||||
|
||||
**默认产品假设(实施前需客户签字确认):**
|
||||
|
||||
| 项 | 默认 |
|
||||
|----|------|
|
||||
| 超管 | `super_admin` 可删除任意业务数据 |
|
||||
| 系统管理 | 租户/用户/角色/菜单/部门/字典/站点/职位/流程定义/请假 — **不适用**创建人规则 |
|
||||
| 子表 | 跟进、联系人、开票信息、商机产品等 — **适用**创建人规则 |
|
||||
| 历史空 `create_by` | 仅 `super_admin` 可删;普通用户不可删 |
|
||||
| 比对字段 | 后端用 `user_id`(UUID);前端用 `row.createBy`,禁止用 `realName` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 统计摘要
|
||||
|
||||
| 类别 | 后端删除接口(约) | 前端含「删除」页面(约) |
|
||||
|------|-------------------|-------------------------|
|
||||
| CRM 业务 | 18 | 22 |
|
||||
| PL 物流 | 8 | 9 |
|
||||
| DB 地磅 | 3 | 2 |
|
||||
| BX 报销 | 5 | 5 |
|
||||
| 系统管理 | 11 | 11 |
|
||||
| **UI 本地删除(不改)** | — | ~8 组件/页 |
|
||||
| **合计(业务范围内)** | **34** | **38** |
|
||||
|
||||
---
|
||||
|
||||
## 2. 需改造对照表(业务数据 — 纳入范围)
|
||||
|
||||
### 2.1 CRM
|
||||
|
||||
| # | 实体 | 后端接口 | 后端实现位置 | 前端 API | 前端页面 / 入口 | 批量删除 | 现状 |
|
||||
|---|------|----------|--------------|----------|-----------------|----------|------|
|
||||
| C01 | 线索 | `POST /web/crm/clue/delete` | `crm.py` → `soft_delete_clue` | `delClue` | `crm/clue/index.vue` 行删 | 否 | 仅 data_scope |
|
||||
| C02 | 线索 | 同上 | 同上 | `delClue` | `crm/clue/detailDrawer.vue` 主删 | — | 无创建人校验 |
|
||||
| C03 | 线索跟进 | `POST /web/crm/clue/follow/delete` | `delete_clue_follow` | `delClueFollow` | `detailDrawer.vue` 跟进删 | 否 | 前端 `canManageFollow`(createBy);后端 data_scope |
|
||||
| C04 | 客户 | `POST /web/crm/customer/delete` | `soft_delete_customer` | `fetchDeleteCustomer` | `crm/customer/index.vue` | 否 | 无 |
|
||||
| C05 | 客户 | 同上 | 同上 | 同上 | `CustomerDetailDialog.vue` 主删 | — | 无 |
|
||||
| C06 | 客户跟进 | `POST /web/crm/customer/follow/delete` | `soft_delete_follow` | `fetchDeleteCustomerFollow` | `CustomerDetailDialog.vue` | 否 | 前端 realName+admin(脆弱) |
|
||||
| C07 | 联系人 | `POST /web/crm/contact/delete` | `soft_delete_contact` | `fetchDeleteContact` | `CustomerDetailDialog.vue`、`ContactList.vue` | 否 | 无 |
|
||||
| C08 | 开票信息 | `POST /web/crm/invoice-info/delete` | `soft_delete_invoice_info` | `delInvoiceInfo` | `InvoiceInfo.vue` | 否 | 无 |
|
||||
| C09 | 商机 | `POST /web/crm/opportunity/delete` | `soft_delete_opportunities` | `delOpportunity` | `opportunity/index.vue` | 否 | 无 |
|
||||
| C10 | 商机 | 同上 | 同上 | 同上 | `OpportunityDetailDrawer.vue` 主删 | — | 无 |
|
||||
| C11 | 商机跟进 | `POST /web/crm/opportunity/follow/delete` | `soft_delete_opportunity_follow` | `delOpportunityFollow` | `OpportunityDetailDrawer.vue` | 否 | 无 |
|
||||
| C12 | 商机产品 | `POST /web/crm/opportunity/product/delete` | `soft_delete_opportunity_products` | `delOpportunityProduct` | 表单内(若有持久化删) | 否 | 无 |
|
||||
| C13 | 报价 | `POST /web/crm/quote/delete` | `soft_delete_quotes` | `delQuote` | `quote/index.vue` | 是 | 无 |
|
||||
| C14 | 报价 | 同上 | 同上 | 同上 | `quote/detail.vue`、`QuoteDetailDialog.vue` | — | 无 |
|
||||
| C15 | 报价产品 | `POST /web/crm/quote/product/delete` | `soft_delete_quote_products` | `delQuoteProduct` | 已保存明细删(若有) | 否 | 无;表单内行删见 §3 |
|
||||
| C16 | 合同 | `DELETE /web/crm/contract/{ids}` | `soft_delete_contracts` | `delContract` | `contract/index.vue` | 是 | 无 |
|
||||
| C17 | 合同 | 同上 | 同上 | 同上 | `contract/detail.vue`、`ContractDetailDialog.vue` | — | 无 |
|
||||
| C18 | 合同变更 | `DELETE /web/crm/contract/change/{ids}` | `soft_delete_contract_changes` | `delChange` | `contract/change/index.vue` | 是 | 无 |
|
||||
| C19 | 合同模板 | `DELETE /web/crm/contract/template/{ids}` | `soft_delete_contract_templates` | `delTemplate` | `contract/template/index.vue` | 是 | 无 |
|
||||
| C20 | 产品 | `POST /web/crm/products/delete` | `soft_delete_product` | `fetchDeleteProduct` | `crm/product/index.vue` | 是 | 无 |
|
||||
| C21 | 产品分类 | `POST /web/crm/product-categories/delete` | `soft_delete_category` | `fetchDeleteProductCategory` | `crm/product-category/index.vue` | 否 | 无 |
|
||||
| C22 | 公海规则 | `POST /web/crm/customer/pool/rule/delete` | `delete_pool_rules` | `fetchDeletePoolRule` | `customer/pool/RuleIndex.vue` | 否 | 无 |
|
||||
| C23 | 供应商信息 | `POST /web/crm/supplierInfo/delete` | `soft_delete_supplier_info` | `delSupplierInfo` | **暂无独立列表删入口** | — | 预留后端 |
|
||||
|
||||
### 2.2 PL 物流
|
||||
|
||||
| # | 实体 | 后端接口 | 后端实现位置 | 前端 API | 前端页面 | 批量 | 现状 |
|
||||
|---|------|----------|--------------|----------|----------|------|------|
|
||||
| P01 | 承运商 | `DELETE /transport/carrier/{id}` | `pl/base_data.py` | `carrierApi.delete` | `pl/carrier/index.vue` | 否 | data_scope |
|
||||
| P02 | 车辆 | `DELETE /transport/vehicle/{id}` | 同上 | `vehicleApi.delete` | `pl/vehicle/index.vue` | 否 | data_scope |
|
||||
| P03 | 自有车辆 | 同上 | 同上 | 同上 | `pl/self-vehicle/index.vue` | 否 | data_scope |
|
||||
| P04 | 人员 | `DELETE /transport/personnel/{id}` | 同上 | `personnelApi.delete` | `pl/personnel/index.vue` | 否 | data_scope |
|
||||
| P05 | 司机 | `DELETE /transport/driver/{id}` | 同上 | `driverApi.delete` | `pl/driver/index.vue` | 否 | data_scope |
|
||||
| P06 | 车辆-司机关联 | `DELETE /transport/vehicle-driver/{id}` | 同上 | (内嵌于车辆/人员维护) | 车辆/人员表单内 | 否 | data_scope |
|
||||
| P07 | 运输计划 | `DELETE /transport/plan/{id}` | `pl/plan.py` | `planApi.delete` | `pl/plan/index.vue` | 否 | data_scope |
|
||||
| P08 | 运费结算单 | `DELETE /transport/freight/{id}` | `pl/freight.py` → `delete_draft_settlement` | `freightApi.delete` | `pl/freight/SettlementBillPanel.vue` | 否 | 仅草稿;无 create_by 校验 |
|
||||
|
||||
### 2.3 DB 地磅
|
||||
|
||||
| # | 实体 | 后端接口 | 后端实现位置 | 前端 API | 前端页面 | 现状 |
|
||||
|---|------|----------|--------------|----------|----------|------|
|
||||
| D01 | 库存记录 | `DELETE /inventory/{id}` | `db/inventory.py` | `inventoryApi.delete` | `db/inventory/index.vue` | 前端日期规则;后端 data_scope |
|
||||
| D02 | 地磅计划 | `DELETE /plans/{id}` | `db/plans.py` | `plansApi.delete` | `db/plans/index.vue` | data_scope |
|
||||
| D03 | 过磅记录 | `DELETE /weighing/{id}` | `db/weighing.py` | `weighingApi.delete` | 公开页若有 | data_scope |
|
||||
|
||||
### 2.4 BX 报销
|
||||
|
||||
| # | 实体 | 后端接口 | 后端实现位置 | 前端 API | 前端页面 | 现状 |
|
||||
|---|------|----------|--------------|----------|----------|------|
|
||||
| B01 | 报销申请 | `DELETE /reimburses/{id}` | `bx/reimburse.py` → `ReimburseService.soft_delete` | `reimburseApi.delete` | `bx/reimburse/apply/index.vue` | data_scope |
|
||||
| B02 | 预支申请 | `DELETE /advances/{id}` | `bx/advance.py` | `advanceApi.delete` | `bx/advance/apply/index.vue` | data_scope |
|
||||
| B03 | 费用项 | `DELETE /expense-items/{id}` | `bx/expense_item.py` | `expenseItemApi.delete` | `bx/expense-item/index.vue` | data_scope |
|
||||
| B04 | 附件 | `DELETE /attachments/{id}` | `bx/attachment.py` | `attachmentApi.delete` | `BxAttachmentUpload` 等 | 随主单;删附件也需创建人 |
|
||||
| B05 | BX 工作流配置 | `DELETE /workflows/{id}` | `bx/workflow.py` | `workflowApi.delete` | `bx/workflow/reimburse|advance/index.vue` | **建议归入系统配置,默认不改造** |
|
||||
|
||||
---
|
||||
|
||||
## 3. 不纳入改造(UI 本地 / 非持久化删除)
|
||||
|
||||
| 页面 / 组件 | 说明 |
|
||||
|-------------|------|
|
||||
| `QuoteFormPanel.vue` `handleDeleteProducts` / `handleRemoveProduct` | 编辑表单内未提交行,非 API 删库 |
|
||||
| `contract/form.vue` 行项删除 | 同上 |
|
||||
| `ContractAttachmentPanel.vue` | 表单附件列表本地 splice |
|
||||
| `attachment-upload-field`、`BxAttachmentUpload` | 未保存附件卡片 |
|
||||
| `FlowDesigner.vue` | 流程设计器节点/连线 |
|
||||
| `system/user/detail.vue` 地址删除 | 本地 mock 数组 |
|
||||
| `art-global-search` 搜索历史 | 本地 storage |
|
||||
|
||||
---
|
||||
|
||||
## 4. 默认排除(系统管理 — 待客户确认是否纳入)
|
||||
|
||||
| # | 实体 | 后端接口 | 前端页面 |
|
||||
|---|------|----------|----------|
|
||||
| S01 | 租户 | `DELETE /web/admin/tenants/{id}` | `system/tenant/index.vue` |
|
||||
| S02 | 用户 | `DELETE /web/admin/users/{id}` | `system/user/index.vue` |
|
||||
| S03 | 角色 | `DELETE /web/admin/role-management/{id}` | `system/role/index.vue` |
|
||||
| S04 | 菜单 | `DELETE /web/admin/menus/{id}` | `system/menu/index.vue` |
|
||||
| S05 | 部门 | `DELETE /web/admin/depts/{id}` | `system/dept/index.vue` |
|
||||
| S06 | 字典类型/数据 | `DELETE .../dict-types|dict-data/{id}` | `system/dict/index.vue` |
|
||||
| S07 | 站点 | `DELETE /web/admin/sites/{id}` | `system/site/index.vue` |
|
||||
| S08 | 职位 | `DELETE /web/admin/posts/{id}` | `system/post/index.vue` |
|
||||
| S09 | 流程定义 | `DELETE /web/admin/workflow/defs/{id}` | `system/workflow/process/index.vue` |
|
||||
| S10 | 请假 | `DELETE /web/admin/leave/{id}` | `system/leave/index.vue` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 分阶段改造方案
|
||||
|
||||
### Phase 0 — 基础设施(必须先做)
|
||||
|
||||
**后端**
|
||||
|
||||
- [ ] 新建 `backend/app/core/delete_guard.py`
|
||||
- `assert_row_deletable_by_creator(row, user_info, *, message="仅创建人可删除")`
|
||||
- `assert_rows_deletable_by_creator(rows, user_info)` — 批量时全量校验
|
||||
- `super_admin` bypass;`create_by` 为空时仅超管可删
|
||||
- [ ] 单元测试 `backend/tests/test_delete_guard.py`(创建人匹配 / 不匹配 / 超管 / 空 create_by)
|
||||
|
||||
**前端**
|
||||
|
||||
- [ ] 新建 `web-admin/src/utils/creator-delete.ts`
|
||||
- `getCurrentUserId()` — 从 `useUserStore().info` 取 `userId` / `id`
|
||||
- `canDeleteByCreator(row, currentUserId?)` — 比对 `row.createBy`
|
||||
- `filterDeletableRows(rows)` — 批量删前过滤
|
||||
- `CREATOR_DELETE_TIP = '仅创建人可删除'`
|
||||
- [ ] 可选:`web-admin/src/components/business/creator-delete-button/index.vue` — 统一 disabled + tooltip
|
||||
|
||||
**文档**
|
||||
|
||||
- [ ] OpenSpec 变更提案:`openspec/changes/creator-only-delete/`(permission + api conventions 各一条 Requirement)
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — CRM 主实体(优先级最高)
|
||||
|
||||
**后端(每个 soft_delete 入口加 assert)**
|
||||
|
||||
- [ ] C01–C02 线索 — `soft_delete_clue`
|
||||
- [ ] C04–C05 客户 — `soft_delete_customer`
|
||||
- [ ] C09–C10 商机 — `soft_delete_opportunities`
|
||||
- [ ] C13–C14 报价 — `soft_delete_quotes`
|
||||
- [ ] C16–C17 合同 — `soft_delete_contracts`
|
||||
- [ ] C18 合同变更 — `soft_delete_contract_changes`
|
||||
- [ ] C19 合同模板 — `soft_delete_contract_templates`
|
||||
|
||||
**前端**
|
||||
|
||||
- [ ] 上表对应 10 个页面/抽屉:行删 / 工具栏删 `v-if="canDeleteByCreator(row)"`
|
||||
- [ ] 批量删(报价/合同/变更/模板):选中时过滤 + `ElMessage.warning` 提示跳过条数
|
||||
- [ ] 列表接口确认返回 `createBy`(缺则补 `_row_to_dict`)
|
||||
|
||||
**验收**
|
||||
|
||||
- [ ] 用户 A 创建、用户 B(同部门、有删权限)调用 API 返回 403
|
||||
- [ ] 用户 A 可删自己的数据;超管可删任意
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — CRM 子实体 + 主数据
|
||||
|
||||
**后端**
|
||||
|
||||
- [ ] C03/C06–C08/C11–C12 跟进、联系人、开票、商机产品
|
||||
- [ ] C20–C22 产品、分类、公海规则
|
||||
- [ ] C23 供应商(仅后端,无 UI 也先加固)
|
||||
|
||||
**前端**
|
||||
|
||||
- [ ] 替换 `CustomerDetailDialog` 跟进删:`realName` → `canDeleteByCreator`
|
||||
- [ ] 统一 `clue/detailDrawer` `canManageFollow` 改用公共 util
|
||||
- [ ] `InvoiceInfo`、`ContactList`、`product`、`product-category`、`RuleIndex`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — PL + DB
|
||||
|
||||
**后端**
|
||||
|
||||
- [ ] P01–P08:`delete_carrier/vehicle/personnel/driver/plan` + `delete_draft_settlement` 加 create_by
|
||||
- [ ] P06 `vehicle-driver` 删关联
|
||||
- [ ] D01–D03 库存/计划/过磅
|
||||
|
||||
**前端**
|
||||
|
||||
- [ ] PL 9 页 + DB 2 页按钮控制
|
||||
- [ ] `db/inventory`:**保留**现有日期规则,与创建人规则 **AND** 关系
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — BX 报销
|
||||
|
||||
**后端**
|
||||
|
||||
- [ ] B01–B04:在 `BxCrudService.soft_delete` 或各 Service 包装层加 create_by(注意 BX 部分历史 `create_by` 存 name 的脏数据,需迁移或兼容比对)
|
||||
|
||||
**前端**
|
||||
|
||||
- [ ] `bx/reimburse/apply`、`bx/advance/apply`、`bx/expense-item`、附件组件
|
||||
|
||||
**备注:** B05 BX 工作流配置默认跳过;若客户要求纳入,单独开 Phase 4b。
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 — 系统管理(可选,需客户确认)
|
||||
|
||||
- [ ] 若确认纳入:S01–S10 后端 + 11 个 system 页面
|
||||
- [ ] 若不纳入:在帮助文档注明「系统管理不受创建人删除限制」
|
||||
|
||||
---
|
||||
|
||||
## 6. 批量删除统一行为
|
||||
|
||||
| 场景 | 前端 | 后端 |
|
||||
|------|------|------|
|
||||
| 选中含他人数据 | 禁用批量删或只提交本人 id 并提示 | 对每条 assert;部分失败返回明细 |
|
||||
| 选中全无 createBy | 普通用户禁用;超管可删 | 空 create_by 仅超管 |
|
||||
| 混合选中 | 推荐:过滤后仅删本人,Message 汇总 | 事务:建议**全失败**或**逐条**(与 CRM 现有 delClue 部分失败风格对齐) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施顺序与估时
|
||||
|
||||
| Phase | 范围 | 后端 | 前端 | 估时 |
|
||||
|-------|------|------|------|------|
|
||||
| 0 | 基础设施 | 0.5d | 0.5d | 1d |
|
||||
| 1 | CRM 主实体 | 1d | 1.5d | 2.5d |
|
||||
| 2 | CRM 子实体 | 1d | 1d | 2d |
|
||||
| 3 | PL + DB | 1d | 1d | 2d |
|
||||
| 4 | BX | 1d | 0.5d | 1.5d |
|
||||
| 5 | 系统管理(可选) | 1d | 1d | 2d |
|
||||
|
||||
**建议迭代:** 每 Phase 独立 PR,先后端再前端,每模块手工回归 2 用户(创建人 / 非创建人)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 下一步
|
||||
|
||||
1. 客户确认 § 默认产品假设 + §4 系统管理是否纳入
|
||||
2. 执行 **Phase 0** 并合入主分支
|
||||
3. 按 Phase 1 → 4 逐步交付,每阶段更新本文件 checkbox
|
||||
@@ -0,0 +1,833 @@
|
||||
# 车辆 ETC/加油充值 — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在运输管理下新增「ETC加油申请」「ETC加油发放登记」两个独立菜单,支持申请提交(无审批)、发放登记、申请-发放关联与单号自动生成。
|
||||
|
||||
**Architecture:** 两张业务表 `pl_vehicle_recharge_apply` / `pl_vehicle_recharge_record`;业务逻辑集中在 `pl_vehicle_recharge_service.py`(单号生成、状态联动、校验);`vehicle_recharge.py` 薄路由;前端两页 P01 标准列表 + 弹窗,复用 `AttachmentUploadField` 与 `SystemViewDialog`。
|
||||
|
||||
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Element Plus + `web-admin/src/api/pl/index.ts`
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/migrations/2026-06-29-pl-vehicle-recharge.sql` | DDL + 字典 + 菜单 + 按钮权限 |
|
||||
| `docs/sql/pl_tables.sql` | 同步两张新表定义 |
|
||||
| `backend/app/models/pl_models.py` | `PlVehicleRechargeApply` / `PlVehicleRechargeRecord` |
|
||||
| `backend/app/schemas/pl/vehicle_recharge.py` | Create/Update/Read/ListResponse |
|
||||
| `backend/app/services/pl_vehicle_recharge_service.py` | 单号、校验、CRUD、状态联动 |
|
||||
| `backend/app/api/pl/vehicle_recharge.py` | REST 路由 |
|
||||
| `backend/app/api/pl/__init__.py` | 注册 router |
|
||||
| `web-admin/src/api/pl/index.ts` | TS 类型 + API 方法 |
|
||||
| `web-admin/src/utils/pl-display.ts` | 充值类型/申请状态 Tag 映射 |
|
||||
| `web-admin/src/views/pl/vehicle-recharge-apply/index.vue` | 申请列表页 |
|
||||
| `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | 发放登记页 |
|
||||
| `web-admin/src/App.vue` | `panelTitleBarTextRules` 注册标题竖条 |
|
||||
|
||||
**API 前缀(与现有 PL 一致):** `/api/transport/vehicle-recharge-apply/*`、`/api/transport/vehicle-recharge-record/*`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: DDL + 字典 + 菜单权限
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/migrations/2026-06-29-pl-vehicle-recharge.sql`
|
||||
- Modify: `docs/sql/pl_tables.sql`(追加两表 CREATE)
|
||||
|
||||
- [ ] **Step 1:** 创建迁移脚本,含两张表(内容见 spec §数据模型):
|
||||
|
||||
```sql
|
||||
-- docs/sql/migrations/2026-06-29-pl-vehicle-recharge.sql
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pl_vehicle_recharge_apply` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`apply_no` varchar(32) NOT NULL COMMENT '申请单号 SXSQ+YYYYMMDD+6位',
|
||||
`license_plate` varchar(20) NOT NULL COMMENT '车牌号',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`apply_date` date NOT NULL COMMENT '申请日期',
|
||||
`odometer_km` decimal(10,1) DEFAULT NULL COMMENT '期初公里数',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`applicant_id` varchar(64) NOT NULL COMMENT '申请人用户ID',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending-待发放 disbursed-已发放 cancelled-已作废',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_apply_no` (`apply_no`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_license_plate` (`license_plate`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_date` (`apply_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值申请';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pl_vehicle_recharge_record` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`recharge_order_no` varchar(32) NOT NULL COMMENT '充值订单号 SXCL+YYYYMMDD+6位',
|
||||
`recharge_date` date NOT NULL COMMENT '充值日期',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`card_no` varchar(64) DEFAULT NULL COMMENT '卡号',
|
||||
`gas_station` varchar(200) DEFAULT NULL COMMENT '加油站/充值渠道',
|
||||
`amount` decimal(10,2) NOT NULL COMMENT '充值金额(元)',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`apply_id` varchar(64) DEFAULT NULL COMMENT '关联申请单ID',
|
||||
`license_plate` varchar(20) DEFAULT NULL COMMENT '车牌号冗余',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0',
|
||||
`order_num` int(11) DEFAULT NULL,
|
||||
`remark` varchar(512) DEFAULT NULL,
|
||||
`create_by` varchar(64) DEFAULT NULL,
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_by` varchar(64) DEFAULT NULL,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_recharge_order_no` (`recharge_order_no`),
|
||||
UNIQUE KEY `uk_apply_id` (`apply_id`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_recharge_date` (`recharge_date`),
|
||||
KEY `idx_license_plate` (`license_plate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值发放登记';
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 同一脚本插入字典类型与字典项:
|
||||
|
||||
```sql
|
||||
INSERT INTO `sys_dict_type` (`id`, `dict_name`, `dict_type`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
|
||||
('PL_RECHARGE_TYPE', '充值类型', 'pl_recharge_type', 'shanxia2.0', 10, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_RECHARGE_APPLY_STATUS', '充值申请状态', 'pl_recharge_apply_status', 'shanxia2.0', 11, NULL, 'system', '', NOW(), NULL, NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `dict_name` = VALUES(`dict_name`), `update_time` = NOW(), `del_flag` = 0;
|
||||
|
||||
INSERT INTO `sys_dict_data` (`id`, `dict_label`, `dict_value`, `tag_type`, `dict_type`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
|
||||
('PL_RECHARGE_TYPE_ETC', 'ETC', 'etc', 'primary', 'pl_recharge_type', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_RECHARGE_TYPE_FUEL', '加油', 'fuel', 'success', 'pl_recharge_type', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_RECHARGE_APPLY_PENDING', '待发放', 'pending', 'warning', 'pl_recharge_apply_status', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_RECHARGE_APPLY_DISBURSED', '已发放', 'disbursed', 'success', 'pl_recharge_apply_status', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_RECHARGE_APPLY_CANCELLED', '已作废', 'cancelled', 'info', 'pl_recharge_apply_status', 'shanxia2.0', 3, NULL, 'system', '', NOW(), NULL, NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `dict_label` = VALUES(`dict_label`), `tag_type` = VALUES(`tag_type`), `update_time` = NOW(), `del_flag` = 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** 菜单与按钮权限(parent `PL_MENU_ROOT`,order_num 31/32):
|
||||
|
||||
```sql
|
||||
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
|
||||
('PL_VEH_RECHARGE_APPLY', 'ETC加油申请', 'PL_MENU_ROOT', 'C', 'vehicle-recharge-apply', 'pl/vehicle-recharge-apply/index', 'pl:vehicle-recharge-apply:list', 1, '1', '1', 'shanxia2.0', 31, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VEH_RECHARGE_RECORD', 'ETC加油发放登记', 'PL_MENU_ROOT', 'C', 'vehicle-recharge-record', 'pl/vehicle-recharge-record/index', 'pl:vehicle-recharge-record:list', 1, '1', '1', 'shanxia2.0', 32, NULL, 'system', '', NOW(), NULL, NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `menu_name` = VALUES(`menu_name`), `component` = VALUES(`component`), `perms` = VALUES(`perms`), `update_time` = NOW(), `del_flag` = 0;
|
||||
|
||||
-- 申请按钮
|
||||
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
|
||||
('PL_VRA_Q', '查询', 'PL_VEH_RECHARGE_APPLY', 'F', '', '', 'pl:vehicle-recharge-apply:query', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRA_A', '新增', 'PL_VEH_RECHARGE_APPLY', 'F', '', '', 'pl:vehicle-recharge-apply:add', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRA_E', '编辑', 'PL_VEH_RECHARGE_APPLY', 'F', '', '', 'pl:vehicle-recharge-apply:edit', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRA_R', '删除', 'PL_VEH_RECHARGE_APPLY', 'F', '', '', 'pl:vehicle-recharge-apply:remove', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRA_C', '作废', 'PL_VEH_RECHARGE_APPLY', 'F', '', '', 'pl:vehicle-recharge-apply:cancel', 1, '1', '1', 'shanxia2.0', 5, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
-- 发放按钮
|
||||
('PL_VRR_Q', '查询', 'PL_VEH_RECHARGE_RECORD', 'F', '', '', 'pl:vehicle-recharge-record:query', 1, '1', '1', 'shanxia2.0', 1, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRR_A', '新增', 'PL_VEH_RECHARGE_RECORD', 'F', '', '', 'pl:vehicle-recharge-record:add', 1, '1', '1', 'shanxia2.0', 2, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRR_E', '编辑', 'PL_VEH_RECHARGE_RECORD', 'F', '', '', 'pl:vehicle-recharge-record:edit', 1, '1', '1', 'shanxia2.0', 3, NULL, 'system', '', NOW(), NULL, NOW(), 0),
|
||||
('PL_VRR_R', '删除', 'PL_VEH_RECHARGE_RECORD', 'F', '', '', 'pl:vehicle-recharge-record:remove', 1, '1', '1', 'shanxia2.0', 4, NULL, 'system', '', NOW(), NULL, NOW(), 0)
|
||||
ON DUPLICATE KEY UPDATE `perms` = VALUES(`perms`), `update_time` = NOW(), `del_flag` = 0;
|
||||
```
|
||||
|
||||
- [ ] **Step 4:** 将两表 DDL 追加到 `docs/sql/pl_tables.sql` 末尾(与 migration 一致)
|
||||
|
||||
- [ ] **Step 5:** 在目标库执行迁移并验证
|
||||
|
||||
Run: `mysql -u... -p... shanxia -e "SHOW TABLES LIKE 'pl_vehicle_recharge%';"`
|
||||
Expected: 2 rows
|
||||
|
||||
- [ ] **Step 6:** Commit
|
||||
|
||||
```bash
|
||||
git add docs/sql/migrations/2026-06-29-pl-vehicle-recharge.sql docs/sql/pl_tables.sql
|
||||
git commit -m "feat(pl): add vehicle recharge apply/record DDL and menus"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: ORM 模型
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/models/pl_models.py`
|
||||
|
||||
- [ ] **Step 1:** 在 `PlDocumentReminder` 类之后追加两个模型(import 已有 `Date`, `DECIMAL`, `datetime`):
|
||||
|
||||
```python
|
||||
class PlVehicleRechargeApply(Base):
|
||||
"""车辆ETC/加油充值申请 pl_vehicle_recharge_apply"""
|
||||
|
||||
__tablename__ = "pl_vehicle_recharge_apply"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
apply_no = Column(String(32), nullable=False, unique=True, comment="申请单号")
|
||||
license_plate = Column(String(20), nullable=False, comment="车牌号")
|
||||
recharge_type = Column(String(20), nullable=False, comment="充值类型 etc/fuel")
|
||||
apply_date = Column(Date, nullable=False, comment="申请日期")
|
||||
odometer_km = Column(DECIMAL(10, 1), nullable=True, comment="期初公里数")
|
||||
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
|
||||
applicant_id = Column(String(64), nullable=False, comment="申请人ID")
|
||||
status = Column(String(20), nullable=False, default="pending", comment="申请状态")
|
||||
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
|
||||
|
||||
class PlVehicleRechargeRecord(Base):
|
||||
"""车辆ETC/加油充值发放登记 pl_vehicle_recharge_record"""
|
||||
|
||||
__tablename__ = "pl_vehicle_recharge_record"
|
||||
|
||||
id = Column(String(64), primary_key=True, comment="主键ID")
|
||||
recharge_order_no = Column(String(32), nullable=False, unique=True, comment="充值订单号")
|
||||
recharge_date = Column(Date, nullable=False, comment="充值日期")
|
||||
recharge_type = Column(String(20), nullable=False, comment="充值类型")
|
||||
card_no = Column(String(64), nullable=True, comment="卡号")
|
||||
gas_station = Column(String(200), nullable=True, comment="加油站/渠道")
|
||||
amount = Column(DECIMAL(10, 2), nullable=False, comment="充值金额")
|
||||
attachment_urls = Column(String(2000), nullable=True, comment="附件URL")
|
||||
apply_id = Column(String(64), nullable=True, unique=True, comment="关联申请单ID")
|
||||
license_plate = Column(String(20), nullable=True, comment="车牌号冗余")
|
||||
tenant_no = Column(String(64), nullable=False, default="shanxia2.0", comment="租户编号")
|
||||
order_num = Column(Integer, nullable=True, comment="排序号")
|
||||
remark = Column(String(512), nullable=True, comment="备注")
|
||||
create_by = Column(String(64), nullable=True, comment="创建人id")
|
||||
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
|
||||
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
|
||||
update_by = Column(String(64), nullable=True, comment="更新人id")
|
||||
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
|
||||
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 验证 import
|
||||
|
||||
Run: `cd backend && python3 -c "from app.models.pl_models import PlVehicleRechargeApply, PlVehicleRechargeRecord; print('ok')"`
|
||||
Expected: `ok`
|
||||
|
||||
- [ ] **Step 3:** Commit
|
||||
|
||||
```bash
|
||||
git add backend/app/models/pl_models.py
|
||||
git commit -m "feat(pl): add vehicle recharge ORM models"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Pydantic Schema
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/schemas/pl/vehicle_recharge.py`
|
||||
- Modify: `backend/app/schemas/pl/__init__.py`
|
||||
|
||||
- [ ] **Step 1:** 新建 schema 文件:
|
||||
|
||||
```python
|
||||
"""车辆 ETC/加油充值 Schema"""
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
RECHARGE_TYPES = {"etc", "fuel"}
|
||||
APPLY_STATUSES = {"pending", "disbursed", "cancelled"}
|
||||
|
||||
|
||||
class VehicleRechargeApplyCreate(BaseModel):
|
||||
license_plate: str = Field(..., alias="licensePlate")
|
||||
recharge_type: str = Field(..., alias="rechargeType")
|
||||
apply_date: date = Field(..., alias="applyDate")
|
||||
odometer_km: Optional[Decimal] = Field(default=None, alias="odometerKm")
|
||||
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
|
||||
remark: Optional[str] = None
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
@field_validator("recharge_type")
|
||||
@classmethod
|
||||
def validate_recharge_type(cls, v: str) -> str:
|
||||
if v not in RECHARGE_TYPES:
|
||||
raise ValueError("充值类型无效")
|
||||
return v
|
||||
|
||||
|
||||
class VehicleRechargeApplyUpdate(VehicleRechargeApplyCreate):
|
||||
pass
|
||||
|
||||
|
||||
class VehicleRechargeApplyRead(BaseModel):
|
||||
id: str
|
||||
apply_no: str = Field(..., alias="applyNo")
|
||||
license_plate: str = Field(..., alias="licensePlate")
|
||||
recharge_type: str = Field(..., alias="rechargeType")
|
||||
apply_date: date = Field(..., alias="applyDate")
|
||||
odometer_km: Optional[Decimal] = Field(default=None, alias="odometerKm")
|
||||
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
|
||||
applicant_id: str = Field(..., alias="applicantId")
|
||||
applicant_name: Optional[str] = Field(default=None, alias="applicantName")
|
||||
status: str
|
||||
remark: Optional[str] = None
|
||||
create_time: Optional[datetime] = Field(default=None, alias="createTime")
|
||||
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
|
||||
|
||||
model_config = {"populate_by_name": True, "from_attributes": True}
|
||||
|
||||
|
||||
class VehicleRechargeApplyListResponse(BaseModel):
|
||||
items: list[VehicleRechargeApplyRead]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int = Field(..., alias="pageSize")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class VehicleRechargeApplyOption(BaseModel):
|
||||
id: str
|
||||
apply_no: str = Field(..., alias="applyNo")
|
||||
license_plate: str = Field(..., alias="licensePlate")
|
||||
recharge_type: str = Field(..., alias="rechargeType")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class VehicleRechargeRecordCreate(BaseModel):
|
||||
recharge_date: date = Field(..., alias="rechargeDate")
|
||||
recharge_type: str = Field(..., alias="rechargeType")
|
||||
card_no: Optional[str] = Field(default=None, alias="cardNo")
|
||||
gas_station: Optional[str] = Field(default=None, alias="gasStation")
|
||||
amount: Decimal
|
||||
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
|
||||
apply_id: Optional[str] = Field(default=None, alias="applyId")
|
||||
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
|
||||
remark: Optional[str] = None
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
@field_validator("recharge_type")
|
||||
@classmethod
|
||||
def validate_recharge_type(cls, v: str) -> str:
|
||||
if v not in RECHARGE_TYPES:
|
||||
raise ValueError("充值类型无效")
|
||||
return v
|
||||
|
||||
@field_validator("amount")
|
||||
@classmethod
|
||||
def validate_amount(cls, v: Decimal) -> Decimal:
|
||||
if v is None or v <= 0:
|
||||
raise ValueError("充值金额须大于 0")
|
||||
return v
|
||||
|
||||
|
||||
class VehicleRechargeRecordUpdate(VehicleRechargeRecordCreate):
|
||||
pass
|
||||
|
||||
|
||||
class VehicleRechargeRecordRead(BaseModel):
|
||||
id: str
|
||||
recharge_order_no: str = Field(..., alias="rechargeOrderNo")
|
||||
recharge_date: date = Field(..., alias="rechargeDate")
|
||||
recharge_type: str = Field(..., alias="rechargeType")
|
||||
card_no: Optional[str] = Field(default=None, alias="cardNo")
|
||||
gas_station: Optional[str] = Field(default=None, alias="gasStation")
|
||||
amount: Decimal
|
||||
attachment_urls: Optional[str] = Field(default=None, alias="attachmentUrls")
|
||||
apply_id: Optional[str] = Field(default=None, alias="applyId")
|
||||
apply_no: Optional[str] = Field(default=None, alias="applyNo")
|
||||
license_plate: Optional[str] = Field(default=None, alias="licensePlate")
|
||||
remark: Optional[str] = None
|
||||
create_by: Optional[str] = Field(default=None, alias="createBy")
|
||||
create_by_name: Optional[str] = Field(default=None, alias="createByName")
|
||||
create_time: Optional[datetime] = Field(default=None, alias="createTime")
|
||||
update_time: Optional[datetime] = Field(default=None, alias="updateTime")
|
||||
|
||||
model_config = {"populate_by_name": True, "from_attributes": True}
|
||||
|
||||
|
||||
class VehicleRechargeRecordListResponse(BaseModel):
|
||||
items: list[VehicleRechargeRecordRead]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int = Field(..., alias="pageSize")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 在 `schemas/pl/__init__.py` 导出上述类型并加入 `__all__`
|
||||
|
||||
- [ ] **Step 3:** Commit
|
||||
|
||||
```bash
|
||||
git add backend/app/schemas/pl/vehicle_recharge.py backend/app/schemas/pl/__init__.py
|
||||
git commit -m "feat(pl): add vehicle recharge pydantic schemas"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Service 层(单号 + 业务逻辑)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/services/pl_vehicle_recharge_service.py`
|
||||
|
||||
- [ ] **Step 1:** 实现核心 service(节选关键函数):
|
||||
|
||||
```python
|
||||
"""车辆 ETC/加油充值业务逻辑"""
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.pl_models import (
|
||||
PlVehicle,
|
||||
PlVehicleRechargeApply,
|
||||
PlVehicleRechargeRecord,
|
||||
)
|
||||
from app.models.models import SysUser
|
||||
from app.schemas.pl.vehicle_recharge import (
|
||||
VehicleRechargeApplyCreate,
|
||||
VehicleRechargeApplyUpdate,
|
||||
VehicleRechargeRecordCreate,
|
||||
VehicleRechargeRecordUpdate,
|
||||
)
|
||||
from app.services.pl_data_scope import apply_pl_scope, assert_pl_row_in_scope
|
||||
|
||||
|
||||
def _generate_seq_no(db: Session, model, column, prefix: str) -> str:
|
||||
last = (
|
||||
db.query(model)
|
||||
.filter(getattr(model, column.name).like(f"{prefix}%"))
|
||||
.order_by(getattr(model, column.name).desc())
|
||||
.first()
|
||||
)
|
||||
seq = int(getattr(last, column.key)[-6:]) + 1 if last else 1
|
||||
return f"{prefix}{seq:06d}"
|
||||
|
||||
|
||||
def generate_apply_no(db: Session) -> str:
|
||||
prefix = f"SXSQ{date.today():%Y%m%d}"
|
||||
return _generate_seq_no(db, PlVehicleRechargeApply, PlVehicleRechargeApply.apply_no, prefix)
|
||||
|
||||
|
||||
def generate_recharge_order_no(db: Session) -> str:
|
||||
prefix = f"SXCL{date.today():%Y%m%d}"
|
||||
return _generate_seq_no(db, PlVehicleRechargeRecord, PlVehicleRechargeRecord.recharge_order_no, prefix)
|
||||
|
||||
|
||||
def _resolve_user_name(db: Session, user_id: Optional[str]) -> Optional[str]:
|
||||
if not user_id:
|
||||
return None
|
||||
row = db.query(SysUser).filter(SysUser.id == user_id, SysUser.del_flag == 0).first()
|
||||
return row.nick_name or row.user_name if row else None
|
||||
|
||||
|
||||
def _assert_vehicle_exists(db: Session, tenant_no: str, license_plate: str) -> None:
|
||||
exists = (
|
||||
db.query(PlVehicle.id)
|
||||
.filter(
|
||||
PlVehicle.tenant_no == tenant_no,
|
||||
PlVehicle.license_plate == license_plate,
|
||||
PlVehicle.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not exists:
|
||||
raise HTTPException(status_code=400, detail="车牌号不存在")
|
||||
|
||||
|
||||
def apply_to_dict(row: PlVehicleRechargeApply, db: Session) -> dict:
|
||||
return {
|
||||
"id": row.id,
|
||||
"applyNo": row.apply_no,
|
||||
"licensePlate": row.license_plate,
|
||||
"rechargeType": row.recharge_type,
|
||||
"applyDate": row.apply_date,
|
||||
"odometerKm": row.odometer_km,
|
||||
"attachmentUrls": row.attachment_urls,
|
||||
"applicantId": row.applicant_id,
|
||||
"applicantName": _resolve_user_name(db, row.applicant_id),
|
||||
"status": row.status,
|
||||
"remark": row.remark,
|
||||
"createTime": row.create_time,
|
||||
"updateTime": row.update_time,
|
||||
}
|
||||
|
||||
|
||||
def record_to_dict(row: PlVehicleRechargeRecord, db: Session, apply_no: Optional[str] = None) -> dict:
|
||||
return {
|
||||
"id": row.id,
|
||||
"rechargeOrderNo": row.recharge_order_no,
|
||||
"rechargeDate": row.recharge_date,
|
||||
"rechargeType": row.recharge_type,
|
||||
"cardNo": row.card_no,
|
||||
"gasStation": row.gas_station,
|
||||
"amount": row.amount,
|
||||
"attachmentUrls": row.attachment_urls,
|
||||
"applyId": row.apply_id,
|
||||
"applyNo": apply_no,
|
||||
"licensePlate": row.license_plate,
|
||||
"remark": row.remark,
|
||||
"createBy": row.create_by,
|
||||
"createByName": _resolve_user_name(db, row.create_by),
|
||||
"createTime": row.create_time,
|
||||
"updateTime": row.update_time,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 在同一文件实现 `list_applies` / `create_apply` / `update_apply` / `cancel_apply` / `delete_apply` / `list_pending_apply_options` / `list_records` / `create_record` / `update_record` / `delete_records`,规则:
|
||||
|
||||
- 创建申请:`status='pending'`,`applicant_id=create_by=user_id`
|
||||
- 更新申请:仅 `pending`
|
||||
- 作废:仅 `pending` → `cancelled`
|
||||
- 删除申请:仅 `pending` 且无关联 record(查 `pl_vehicle_recharge_record.apply_id`)
|
||||
- 创建 record:生成 `recharge_order_no`;若 `apply_id` 有值则校验申请 `pending`、类型一致,写入 `license_plate`,申请改 `disbursed`;无 `apply_id` 则 `license_plate` 必填
|
||||
- 删除 record:软删;若有关联申请则回退 `pending`
|
||||
- 所有 list/get 走 `apply_pl_scope` + `assert_pl_row_in_scope`
|
||||
|
||||
- [ ] **Step 3:** 验证 import
|
||||
|
||||
Run: `cd backend && python3 -c "from app.services.pl_vehicle_recharge_service import generate_apply_no; print('ok')"`
|
||||
Expected: `ok`
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add backend/app/services/pl_vehicle_recharge_service.py
|
||||
git commit -m "feat(pl): add vehicle recharge service with status linkage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: API 路由 + 注册
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/app/api/pl/vehicle_recharge.py`
|
||||
- Modify: `backend/app/api/pl/__init__.py`
|
||||
|
||||
- [ ] **Step 1:** 创建两个 `APIRouter`:
|
||||
|
||||
```python
|
||||
apply_router = APIRouter(prefix="/vehicle-recharge-apply", tags=["运输计划-ETC加油申请"])
|
||||
record_router = APIRouter(prefix="/vehicle-recharge-record", tags=["运输计划-ETC加油发放"])
|
||||
|
||||
@apply_router.get("/", response_model=VehicleRechargeApplyListResponse)
|
||||
def list_applies(...): ...
|
||||
|
||||
@apply_router.get("/pending-options", response_model=list[VehicleRechargeApplyOption])
|
||||
def pending_options(...): ...
|
||||
|
||||
@apply_router.get("/{id}", response_model=VehicleRechargeApplyRead)
|
||||
def get_apply(...): ...
|
||||
|
||||
@apply_router.post("/", response_model=VehicleRechargeApplyRead, status_code=201)
|
||||
def create_apply(...): ...
|
||||
|
||||
@apply_router.put("/{id}", response_model=VehicleRechargeApplyRead)
|
||||
def update_apply(...): ...
|
||||
|
||||
@apply_router.post("/{id}/cancel", response_model=VehicleRechargeApplyRead)
|
||||
def cancel_apply(...): ...
|
||||
|
||||
@apply_router.delete("/{ids}")
|
||||
def delete_applies(...): ...
|
||||
|
||||
# record_router 同理:GET /, GET /{id}, POST /, PUT /{id}, DELETE /{ids}
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 在 `backend/app/api/pl/__init__.py` 注册:
|
||||
|
||||
```python
|
||||
from app.api.pl import vehicle_recharge
|
||||
|
||||
pl_router.include_router(vehicle_recharge.apply_router)
|
||||
pl_router.include_router(vehicle_recharge.record_router)
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** 启动后端 smoke test(需本地 DB)
|
||||
|
||||
Run: `curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/docs`
|
||||
Expected: `200`(服务已运行时)
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add backend/app/api/pl/vehicle_recharge.py backend/app/api/pl/__init__.py
|
||||
git commit -m "feat(pl): expose vehicle recharge apply and record APIs"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: 前端 API 与 display 工具
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/pl/index.ts`
|
||||
- Modify: `web-admin/src/utils/pl-display.ts`
|
||||
|
||||
- [ ] **Step 1:** 在 `api/pl/index.ts` 追加类型与方法:
|
||||
|
||||
```typescript
|
||||
export interface VehicleRechargeApply {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
applyDate: string
|
||||
odometerKm?: number | null
|
||||
attachmentUrls?: string
|
||||
applicantId: string
|
||||
applicantName?: string
|
||||
status: 'pending' | 'disbursed' | 'cancelled'
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleRechargeApplyListResponse {
|
||||
items: VehicleRechargeApply[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface VehicleRechargeRecord {
|
||||
id: string
|
||||
rechargeOrderNo: string
|
||||
rechargeDate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
cardNo?: string
|
||||
gasStation?: string
|
||||
amount: number
|
||||
attachmentUrls?: string
|
||||
applyId?: string
|
||||
applyNo?: string
|
||||
licensePlate?: string
|
||||
remark?: string
|
||||
createBy?: string
|
||||
createByName?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export const vehicleRechargeApplyApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleRechargeApplyListResponse>('/transport/vehicle-recharge-apply/', { params }),
|
||||
get: (id: string) => request.get<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`),
|
||||
create: (data: Partial<VehicleRechargeApply>) =>
|
||||
request.post<VehicleRechargeApply>('/transport/vehicle-recharge-apply/', data),
|
||||
update: (id: string, data: Partial<VehicleRechargeApply>) =>
|
||||
request.put<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`, data),
|
||||
cancel: (id: string) => request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}/cancel`),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-recharge-apply/${ids}`),
|
||||
pendingOptions: () =>
|
||||
request.get<Array<{ id: string; applyNo: string; licensePlate: string; rechargeType: string }>>(
|
||||
'/transport/vehicle-recharge-apply/pending-options'
|
||||
)
|
||||
}
|
||||
|
||||
export const vehicleRechargeRecordApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<{ items: VehicleRechargeRecord[]; total: number; page: number; pageSize: number }>(
|
||||
'/transport/vehicle-recharge-record/',
|
||||
{ params }
|
||||
),
|
||||
get: (id: string) => request.get<VehicleRechargeRecord>(`/transport/vehicle-recharge-record/${id}`),
|
||||
create: (data: Partial<VehicleRechargeRecord>) =>
|
||||
request.post<VehicleRechargeRecord>('/transport/vehicle-recharge-record/', data),
|
||||
update: (id: string, data: Partial<VehicleRechargeRecord>) =>
|
||||
request.put<VehicleRechargeRecord>(`/transport/vehicle-recharge-record/${id}`, data),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-recharge-record/${ids}`)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2:** 在 `pl-display.ts` 追加:
|
||||
|
||||
```typescript
|
||||
export function getPlRechargeTypeLabel(type: string) {
|
||||
return type === 'etc' ? 'ETC' : type === 'fuel' ? '加油' : type
|
||||
}
|
||||
|
||||
export function getPlRechargeTypeTagType(type: string) {
|
||||
return type === 'etc' ? 'primary' : type === 'fuel' ? 'success' : 'info'
|
||||
}
|
||||
|
||||
export function getPlRechargeApplyStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待发放',
|
||||
disbursed: '已发放',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
export function getPlRechargeApplyStatusTagType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
disbursed: 'success',
|
||||
cancelled: 'info'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
export const PL_GAS_STATION_PRESETS = ['中石化', '线上etc-app', '百链']
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** Commit
|
||||
|
||||
```bash
|
||||
git add web-admin/src/api/pl/index.ts web-admin/src/utils/pl-display.ts
|
||||
git commit -m "feat(pl): add vehicle recharge frontend API and display helpers"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: ETC加油申请页面
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/pl/vehicle-recharge-apply/index.vue`
|
||||
|
||||
- [ ] **Step 1:** 以 `web-admin/src/views/pl/plan/index.vue` 为骨架,实现 P01 列表页:
|
||||
|
||||
- 筛选:关键词(申请单号/车牌)、充值类型、状态、申请日期范围
|
||||
- 表格列 + 分页(`page` / `pageSize`)
|
||||
- 工具栏:已选 N 项 → 新增申请(primary)
|
||||
- 行操作(`fixed="right"` min-width 280):
|
||||
- `pending`:查看、编辑、作废、删除
|
||||
- 其他:查看
|
||||
- 弹窗 `dialog-form-layout` width 720px,`form-grid-2`,字段见 spec
|
||||
- 车牌:`vehicleApi.list()` 加载 options
|
||||
- 附件:`AttachmentUploadField` `folder="pl/vehicle-recharge/apply"` `limit=5` `accept="image/*,.pdf"`
|
||||
- 申请人:`useUserStore()` 当前用户昵称只读
|
||||
- 查看:`SystemViewDialog` + attachment 区块
|
||||
|
||||
- [ ] **Step 2:** 在 `web-admin/src/App.vue` 的 `panelTitleBarTextRules` 追加:
|
||||
|
||||
```typescript
|
||||
'ETC加油申请',
|
||||
```
|
||||
|
||||
- [ ] **Step 3:** 本地验证页面
|
||||
|
||||
Run: `cd web-admin && pnpm dev`
|
||||
手动:登录 → 运输管理 → ETC加油申请 → 新增一条 → 列表可见、单号生成
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/vehicle-recharge-apply/index.vue web-admin/src/App.vue
|
||||
git commit -m "feat(pl): add ETC fuel recharge apply list page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: ETC加油发放登记页面
|
||||
|
||||
**Files:**
|
||||
- Create: `web-admin/src/views/pl/vehicle-recharge-record/index.vue`
|
||||
|
||||
- [ ] **Step 1:** 同样 P01 骨架,实现发放页:
|
||||
|
||||
- 筛选:关键词(订单号/车牌/卡号)、充值类型、充值日期范围、加油站、关联申请单号
|
||||
- 表格列:订单号、充值日期、类型(Tag)、车牌、卡号、加油站、金额、关联申请单号、创建人
|
||||
- 新增/编辑弹窗:
|
||||
- 充值订单号只读(新建 placeholder「保存后自动生成」)
|
||||
- 关联申请:`vehicleRechargeApplyApi.pendingOptions()`,`@change` 带出 `licensePlate` + `rechargeType` 只读
|
||||
- 无关联时车牌、类型可编辑且必填
|
||||
- 加油站:`el-select` `filterable` `allow-create` + `PL_GAS_STATION_PRESETS`
|
||||
- 金额:`el-input-number` precision=2 min=0.01
|
||||
- 附件 folder=`pl/vehicle-recharge/record`
|
||||
- 创建人只读
|
||||
|
||||
- [ ] **Step 2:** `App.vue` 追加 `'ETC加油发放登记'`
|
||||
|
||||
- [ ] **Step 3:** 联调完整流程
|
||||
|
||||
1. 申请页新建待发放申请
|
||||
2. 发放页关联该申请并保存 → 申请状态变已发放
|
||||
3. 发放页无关联补录一条
|
||||
4. 删除发放 → 申请回退待发放
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/pl/vehicle-recharge-record/index.vue web-admin/src/App.vue
|
||||
git commit -m "feat(pl): add ETC fuel recharge disbursement record page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: 字段一致性 + 图谱 + 设计文档状态
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md`(状态 → 已批准)
|
||||
|
||||
- [ ] **Step 1:** 运行字段一致性检查(若脚本已配置新模块则补充映射后执行)
|
||||
|
||||
Run: `cd backend && python scripts/check_field_consistency.py 2>/dev/null || echo "skip if module not registered yet"`
|
||||
|
||||
- [ ] **Step 2:** 更新知识图谱
|
||||
|
||||
Run: `./scripts/update-graph.sh all`
|
||||
|
||||
- [ ] **Step 3:** 更新 spec 状态为「已批准」
|
||||
|
||||
- [ ] **Step 4:** Commit
|
||||
|
||||
```bash
|
||||
git add docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md
|
||||
git commit -m "docs(pl): mark vehicle recharge design approved after plan"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收清单(Task 9 后逐项勾选)
|
||||
|
||||
- [ ] 新建申请 → `SXSQ` 单号 + 待发放 + 当前用户为申请人
|
||||
- [ ] 待发放申请可编辑/作废/删除;已发放仅查看
|
||||
- [ ] 发放关联申请 → `SXCL` 订单号 + 申请变已发放
|
||||
- [ ] 无关联发放补录成功(手填车牌/类型)
|
||||
- [ ] 删除发放 → 申请回退待发放
|
||||
- [ ] 附件上传/预览正常
|
||||
- [ ] 菜单权限按钮可见(有权限角色)
|
||||
- [ ] 租户隔离有效
|
||||
|
||||
---
|
||||
|
||||
## Spec 覆盖自检
|
||||
|
||||
| Spec 要求 | 对应 Task |
|
||||
|-----------|-----------|
|
||||
| D1 无审批 | Task 4 create_apply 直接 pending |
|
||||
| D2 双菜单 | Task 1 菜单 + Task 7/8 页面 |
|
||||
| D3 1:1 关联 | Task 1 uk_apply_id + Task 4 状态联动 |
|
||||
| D4 无申请补录 | Task 4 create_record apply_id 可空 |
|
||||
| D9/D10 单号规则 | Task 4 generate_* |
|
||||
| D11 加油站下拉+自由填 | Task 6 presets + Task 8 el-select allow-create |
|
||||
| 附件规范 | Task 7/8 AttachmentUploadField |
|
||||
| 数据权限 | Task 4 apply_pl_scope |
|
||||
|
||||
---
|
||||
|
||||
## 工作量
|
||||
|
||||
约 **3~4 人日**(与 spec 估算一致)
|
||||
@@ -0,0 +1,56 @@
|
||||
# 车辆维保申请与处理登记 — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans.
|
||||
|
||||
**Goal:** 在运输管理下新增「车辆维保申请」「车辆维保处理登记」,复用 ETC/加油 架构,含选车维保提醒。
|
||||
|
||||
**Architecture:** `pl_vehicle_maintenance_apply` + `pl_vehicle_maintenance_process`;`pl_vehicle_maintenance_service.py`;两 Vue 页面 + process-board API。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-29-vehicle-maintenance-design.md`
|
||||
**OpenSpec:** `openspec/changes/vehicle-maintenance-management/`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/migrations/2026-06-29-pl-vehicle-maintenance.sql` | DDL + 字典 + 菜单 |
|
||||
| `backend/app/models/pl_models.py` | ORM |
|
||||
| `backend/app/schemas/pl/vehicle_maintenance.py` | Pydantic |
|
||||
| `backend/app/services/pl_vehicle_maintenance_service.py` | 业务逻辑 |
|
||||
| `backend/app/api/pl/vehicle_maintenance.py` | REST |
|
||||
| `web-admin/src/views/pl/vehicle-maintenance-apply/index.vue` | 申请页 |
|
||||
| `web-admin/src/views/pl/vehicle-maintenance-process/index.vue` | 处理页 |
|
||||
| `web-admin/src/api/pl/index.ts` | 前端 API |
|
||||
| `web-admin/src/router/modules/pl.ts` | 静态路由 |
|
||||
|
||||
**API 前缀:** `/api/transport/vehicle-maintenance-apply/*`、`/api/transport/vehicle-maintenance-process/*`
|
||||
|
||||
---
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. SQL 迁移 → ORM → Schema → Service → API
|
||||
2. 前端 API + pl-display → 申请页 → 处理页 → 路由
|
||||
3. 联调选车提醒与 process-board
|
||||
4. graphify 更新
|
||||
|
||||
**参考复制源:** `pl_vehicle_recharge_*`、`vehicle-recharge-apply`、`vehicle-recharge-record`
|
||||
|
||||
---
|
||||
|
||||
## 关键差异对照 ETC
|
||||
|
||||
| 项 | ETC | 维保 |
|
||||
|----|-----|------|
|
||||
| 申请状态 | pending/disbursed/cancelled | pending/**processed**/cancelled |
|
||||
| 处理单号 | SXCL | SXWC |
|
||||
| 金额 | 必填 | **可选** |
|
||||
| 类型 | 固定枚举 | **allow-create** |
|
||||
| 选车 | 无 | **reminders API + 弹窗** |
|
||||
| 车辆状态 | 无 | V1 **不联动** |
|
||||
|
||||
---
|
||||
|
||||
详细任务勾选见 `openspec/changes/vehicle-maintenance-management/tasks.md`。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 车辆尿素申请与发放 — Implementation Plan
|
||||
|
||||
**Goal:** 运输管理下新增「车辆尿素申请」「车辆尿素发放登记」,复用 ETC 发放看板架构。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-06-29-vehicle-urea-design.md`
|
||||
**OpenSpec:** `openspec/changes/vehicle-urea-management/`
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/migrations/2026-06-29-pl-vehicle-urea.sql` | DDL + 字典 + 菜单 |
|
||||
| `backend/app/models/pl_models.py` | ORM |
|
||||
| `backend/app/schemas/pl/vehicle_urea.py` | Schema |
|
||||
| `backend/app/services/pl_vehicle_urea_service.py` | 业务 |
|
||||
| `backend/app/api/pl/vehicle_urea.py` | API |
|
||||
| `web-admin/src/views/pl/vehicle-urea-apply/index.vue` | 申请页 |
|
||||
| `web-admin/src/views/pl/vehicle-urea-disburse/index.vue` | 发放页 |
|
||||
|
||||
## 关键差异(相对 ETC)
|
||||
|
||||
| 项 | ETC | 尿素 |
|
||||
|----|-----|------|
|
||||
| 申请金额 | 无 | **apply_amount 必填** |
|
||||
| 入库字段 | 无 | warehouse_in_date, unit, quantity |
|
||||
| 发放金额 | 必填 | 必填,**默认带出申请金额** |
|
||||
| 状态 | disbursed | disbursed(文案:已发放) |
|
||||
| 单号 | SXSQ/SXCL | **SXNS/SXUF** |
|
||||
|
||||
详细任务见 `openspec/changes/vehicle-urea-management/tasks.md`。
|
||||
@@ -0,0 +1,316 @@
|
||||
# 操作审计与业务语义日志 — 设计文档
|
||||
|
||||
> **日期**: 2026-06-29
|
||||
> **状态**: 已批准(待实现)
|
||||
> **OpenSpec**: `openspec/changes/operation-audit-enhancement/`
|
||||
> **前置**: 现有 `ApiRequestLogMiddleware`、`sys_api_request_log`、`operlog/list` API
|
||||
> **界面模式**: P01 标准列表(操作日志管理页)
|
||||
|
||||
## 目标
|
||||
|
||||
满足客户对「用户操作可追溯」的要求,在**不重建**审计体系的前提下:
|
||||
|
||||
1. **继续全量 API 采集** — 接口地址、入参、出参、时间、操作人
|
||||
2. **补齐请求人姓名与租户** — `create_name`(nick_name)、`tenant_no`
|
||||
3. **增加业务语义字段** — 模块、操作类型、业务主键、可读摘要
|
||||
4. **提供全局管理页** — 筛选、分页、详情
|
||||
5. **CRM 详情 Tab** — 按 `business_id` 精确查日志
|
||||
|
||||
## 已确认业务规则
|
||||
|
||||
| # | 规则 | 决策 |
|
||||
|---|------|------|
|
||||
| D1 | 总体方案 | **方案 A + 语义字段**;不新建 `sys_oper_log` 表 |
|
||||
| D2 | 采集范围 | 默认**全 API**(含 GET 列表);跳过 health/docs/OPTIONS |
|
||||
| D3 | 写操作语义 | `@oper_log` 装饰器声明;只读走路径推断 |
|
||||
| D4 | 请求人展示 | 列表/详情 **姓名优先**(`create_name`),账号为 `username` |
|
||||
| D5 | 入参出参 | 已在中间件记录;详情完整展示(受截断限制) |
|
||||
| D6 | 文件上传 | multipart **不记文件体** |
|
||||
| D7 | 权限 | operlog API 与菜单:**admin / super_admin** + 数据权限 |
|
||||
| D8 | 归档 | 本期只做表结构扩展;定时归档为后续项 |
|
||||
| D9 | business_type | 对齐 `sys_oper_type`:0/1/2/3/5/6 |
|
||||
| D10 | oper_summary 模板 | `{动词}{对象}【{名或编号}】` |
|
||||
|
||||
## 非目标(本期不做)
|
||||
|
||||
- 前端 UI 埋点(不发 API 的行为)
|
||||
- MQ/ES 独立审计平台
|
||||
- 与 CRM Java 仓 `sys_oper_log` 数据合并
|
||||
- 全模块写接口 `@oper_log` 一次性覆盖
|
||||
- multipart 文件内容入库
|
||||
- 响应体手机号/身份证扩展脱敏(待客户确认)
|
||||
|
||||
---
|
||||
|
||||
## 现状与差距
|
||||
|
||||
### 已实现
|
||||
|
||||
| 能力 | 实现位置 |
|
||||
|------|----------|
|
||||
| 中间件采集 | `backend/app/core/api_request_log_middleware.py` |
|
||||
| 落库表 | `sys_api_request_log`、`sys_api_request_log_his` |
|
||||
| 查询 API | `POST /api/web/monitor/operlog/list`、`/detail` |
|
||||
| CRM 嵌入 | `CustomerDetailDialog`、`contract/detail` Tab |
|
||||
| 默认开关 | `API_REQUEST_LOG_ENABLED=true` |
|
||||
|
||||
### 字段映射(客户需求 → 现状)
|
||||
|
||||
| 客户项 | 表字段 | OperLog 字段 | 现状 |
|
||||
|--------|--------|--------------|------|
|
||||
| 接口地址 | `path` | `operUrl` | ✅ |
|
||||
| 入参 | `query_string` + `request_body` | `operParam` | ✅(有例外) |
|
||||
| 出参 | `response_body` | `jsonResult` | ✅(有截断) |
|
||||
| 时间 | `create_at` | `operTime` | ✅ |
|
||||
| 账号 | `username` | `operName` 兜底 | ✅ |
|
||||
| 姓名 | `create_name` | `operName` 优先 | ⚠️ 未写入 |
|
||||
| 模块/类型/单据 | — | `title`、`businessType` | ⚠️ 仅读时推断 |
|
||||
| 可读摘要 | — | — | ❌ |
|
||||
| 租户 | — | — | ❌ |
|
||||
|
||||
### 采集限制(保持)
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| body > 10MB | `[body omitted: N bytes]` |
|
||||
| 文本截断 | `API_REQUEST_LOG_MAX_BODY` 65536 字符 |
|
||||
| 原始读取 | `API_REQUEST_LOG_MAX_BODY_BYTES` 131072 |
|
||||
| 敏感键 | password/token 等 → `***` |
|
||||
| multipart | `[multipart/form-data body omitted]` |
|
||||
|
||||
---
|
||||
|
||||
## 数据模型扩展
|
||||
|
||||
### `sys_api_request_log` / `sys_api_request_log_his` 新增列
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `tenant_no` | varchar(64) | 租户编号,JWT 写入 |
|
||||
| `business_title` | varchar(100) | 模块标题,如 `CRM客户管理` |
|
||||
| `business_type` | tinyint | 0其他 1新增 2修改 3删除 5导出 6导入 |
|
||||
| `business_id` | varchar(64) NULL | 业务主键 |
|
||||
| `oper_summary` | varchar(500) NULL | 人读摘要 |
|
||||
|
||||
**建议索引**:
|
||||
|
||||
- `idx_arl_tenant_createat` (`tenant_no`, `create_at`)
|
||||
- `idx_arl_business` (`business_id`, `business_title`, `create_at`)
|
||||
|
||||
### business_type 枚举
|
||||
|
||||
| 值 | 含义 | 典型场景 |
|
||||
|----|------|----------|
|
||||
| 0 | 其他 | 登录、只读查询 |
|
||||
| 1 | 新增 | create/add |
|
||||
| 2 | 修改 | update/edit/审批/转移 |
|
||||
| 3 | 删除 | delete/remove |
|
||||
| 5 | 导出 | export |
|
||||
| 6 | 导入 | import |
|
||||
|
||||
### 存储样式示例
|
||||
|
||||
**修改客户(有 @oper_log)**
|
||||
|
||||
```
|
||||
tenant_no = shanxia2.0
|
||||
business_title = CRM客户管理
|
||||
business_type = 2
|
||||
business_id = SXCU0000000000000123
|
||||
oper_summary = 修改客户【杭州某某化工有限公司】
|
||||
path = /api/web/crm/customer/update
|
||||
http_method = POST
|
||||
request_body = {"id":"SXCU...","customerName":"杭州某某化工有限公司",...}
|
||||
response_body = {"code":200,"message":"success",...}
|
||||
user_id = d3000000000000000000000000000001
|
||||
username = liming
|
||||
create_name = 李明
|
||||
create_at = 2026-06-29 14:32:05
|
||||
ip_address = 192.168.1.10
|
||||
duration_ms = 45
|
||||
status_code = 200
|
||||
```
|
||||
|
||||
**客户列表(无装饰器,推断兜底)**
|
||||
|
||||
```
|
||||
business_title = CRM客户管理
|
||||
business_type = 0
|
||||
business_id = NULL
|
||||
oper_summary = NULL
|
||||
path = /api/web/crm/customer/list
|
||||
request_body = {"page":1,"pageSize":10}
|
||||
response_body = {"code":200,"data":{...}}
|
||||
```
|
||||
|
||||
**导出(business_id 可空)**
|
||||
|
||||
```
|
||||
business_title = CRM客户管理
|
||||
business_type = 5
|
||||
business_id = NULL
|
||||
oper_summary = 导出客户列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架构与数据流
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Client[Web / 小程序] --> Crypto[ApiCryptoMiddleware]
|
||||
Crypto --> LogMW[ApiRequestLogMiddleware]
|
||||
LogMW --> Route[FastAPI Route]
|
||||
Route --> Decorator["@oper_log → ContextVar"]
|
||||
Decorator --> Service[Service / CRUD]
|
||||
Service --> Route
|
||||
Route --> LogMW
|
||||
LogMW --> DB[(sys_api_request_log)]
|
||||
DB --> OperAPI[operlog/list detail]
|
||||
OperAPI --> AdminPage[操作日志管理页]
|
||||
OperAPI --> CRMTab[CRM 详情 Tab]
|
||||
```
|
||||
|
||||
### 中间件与加解密顺序
|
||||
|
||||
`main.py` 注册顺序:先 `ApiRequestLogMiddleware`,后 `ApiCryptoMiddleware` → Crypto 为外层。RequestLog 在解密后的明文上记录 JSON。
|
||||
|
||||
### ContextVar 与装饰器
|
||||
|
||||
**`app/core/context.py` 扩展**:
|
||||
|
||||
```python
|
||||
oper_log_meta: ContextVar[dict] = ContextVar("oper_log_meta", default=None)
|
||||
```
|
||||
|
||||
**`@oper_log` 参数(建议)**:
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `title` | `business_title` |
|
||||
| `business_type` | int |
|
||||
| `business_id_param` | 从 path/query/body 取 id 的键名 |
|
||||
| `summary` | 固定摘要或带占位符 |
|
||||
| `summary_from` | 从 body 字段组装摘要的可选映射 |
|
||||
|
||||
装饰器在 handler 入口解析参数并 `oper_log_meta.set(...)`;中间件 `finally` 读取后落库。
|
||||
|
||||
**子线程注意**: `_persist_log` 经 `asyncio.to_thread` 执行时,须在主线程先把 ContextVar 值拷贝进 `kwargs`,避免线程内丢失。
|
||||
|
||||
### 路径推断兜底(无装饰器)
|
||||
|
||||
沿用 `oper_log.py` 中 `_MODULE_TITLE_RULES`、`_ACTION_TYPE_MAP`;中间件落库时若 ContextVar 为空则写入推断结果。
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### 现有(增强)
|
||||
|
||||
| 方法 | 路径 | 变更 |
|
||||
|------|------|------|
|
||||
| POST | `/api/web/monitor/operlog/list` | 增加 `businessId`、`businessTitle`、`tenant_no` DB 筛选;`title`/`businessType` 读落库列 |
|
||||
| POST | `/api/web/monitor/operlog/detail` | `operName` = `create_name` 优先 |
|
||||
|
||||
### OperLogListBody 扩展字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `businessId` | string? | 业务主键 |
|
||||
| `businessTitle` | string? | 模块模糊 |
|
||||
| `beginTime` / `endTime` | string? | 时间范围(可选本期) |
|
||||
|
||||
---
|
||||
|
||||
## 前端
|
||||
|
||||
### 操作日志管理页
|
||||
|
||||
| 项 | 值 |
|
||||
|----|-----|
|
||||
| 路径建议 | `web-admin/src/views/system/operlog/index.vue` |
|
||||
| Hash | `/#/system/operlog`(以菜单 SQL 为准) |
|
||||
| 模式 | P01,参考 `views/system/unit/index.vue` |
|
||||
| 权限 | `system:operlog:list`、`system:operlog:query` |
|
||||
|
||||
**列表列**: 模块、操作类型(Tag)、操作人、操作时间、摘要、状态、耗时、操作(详情)
|
||||
|
||||
**筛选**: 模块、操作类型、操作人、状态、时间范围
|
||||
|
||||
**详情弹窗**: 接口地址、IP、入参(`operParam`)、出参(`jsonResult`)、错误信息
|
||||
|
||||
### CRM Tab 改造
|
||||
|
||||
| 页面 | 变更 |
|
||||
|------|------|
|
||||
| `CustomerDetailDialog` | `listOperLog({ businessId: customerId, pageSize: ... })` |
|
||||
| `contract/detail` | `listOperLog({ businessId: contractId, ... })` |
|
||||
|
||||
移除「拉全量 + 前端 filter path/body」逻辑。
|
||||
|
||||
### 菜单 SQL
|
||||
|
||||
挂于「系统管理」下,`order_num` 在字典/岗位等之后;`menuCode` 示例 `system:operlog:list`。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
| 环境变量 | 默认 | 说明 |
|
||||
|----------|------|------|
|
||||
| `API_REQUEST_LOG_ENABLED` | `true` | 生产必须为 true |
|
||||
| `API_REQUEST_LOG_MAX_BODY` | `65536` | 文本字符截断 |
|
||||
| `API_REQUEST_LOG_MAX_BODY_BYTES` | `131072` | 读取字节上限 |
|
||||
| `API_REQUEST_LOG_EXCLUDE_PATHS` | 空(本期新增) | 逗号分隔 path 前缀排除 |
|
||||
|
||||
---
|
||||
|
||||
## 迁移
|
||||
|
||||
**脚本**: `docs/sql/migrations/2026-06-29-operation-audit.sql`
|
||||
|
||||
1. `ALTER TABLE sys_api_request_log` 增加五列 + 索引
|
||||
2. 同步 `ALTER` `sys_api_request_log_his`
|
||||
3. 插入菜单与 `super_admin` 角色菜单(确定性 id)
|
||||
4. 同步 `docs/sql/shanxia2.0.sql`
|
||||
|
||||
---
|
||||
|
||||
## 实施阶段
|
||||
|
||||
| 阶段 | 内容 | 估时 |
|
||||
|------|------|------|
|
||||
| 1 | SQL + ORM + 中间件姓名/租户/语义 | 1 天 |
|
||||
| 2 | ContextVar + `@oper_log` + CRM 写接口样例 | 1 天 |
|
||||
| 3 | operlog API + 管理页 + CRM Tab | 1~2 天 |
|
||||
| 4 | 验收 + graphify | 0.5 天 |
|
||||
|
||||
---
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] 修改客户后 `sys_api_request_log` 含 `create_name`、path、request_body、response_body
|
||||
- [ ] 有 `@oper_log` 的记录含 `business_title`、`business_type`、`business_id`、`oper_summary`
|
||||
- [ ] 管理页可筛选、详情可看入参出参
|
||||
- [ ] 客户详情 Tab 仅显示该 `business_id` 日志
|
||||
- [ ] 非 admin 无法访问 operlog API
|
||||
- [ ] 租户 A 看不到租户 B 日志
|
||||
- [ ] multipart 上传不产生文件二进制日志
|
||||
|
||||
---
|
||||
|
||||
## 待客户确认
|
||||
|
||||
1. **「所有操作」范围** — 当前默认全 API;若仅写操作需配置排除规则
|
||||
2. **日志保留天数** — 建议 90 天热数据 + 归档
|
||||
3. **响应体 PII** — 是否扩展脱敏手机号/身份证
|
||||
|
||||
---
|
||||
|
||||
## 参考
|
||||
|
||||
- `backend/app/core/api_request_log_middleware.py`
|
||||
- `backend/app/crud/monitor/oper_log.py`
|
||||
- `backend/app/api/monitor.py`
|
||||
- `docs/sql/sx.sql` → `sys_oper_log`(RuoYi 对照)
|
||||
- `openspec/changes/operation-audit-enhancement/`
|
||||
@@ -0,0 +1,228 @@
|
||||
# 运输计划 · 承运商资质附件 — 设计文档
|
||||
|
||||
> **日期**: 2026-06-29
|
||||
> **状态**: 已批准
|
||||
> **前置**: `openspec/changes/transport-plan-management/specs/vehicle-driver/spec.md`、`docs/superpowers/specs/2026-06-19-pl-vehicle-personnel-cert-design.md`
|
||||
> **页面**: `web-admin` `/#/pl/carrier`(承运商管理)
|
||||
|
||||
## 目标
|
||||
|
||||
在承运商档案中增加三类**必填**资质/协议附件,并基于**运输协议有效期**控制承运商车辆是否可用于运输安排:
|
||||
|
||||
1. **营业执照**(附件,必填)
|
||||
2. **道路经营许可证**(附件,必填)
|
||||
3. **运输协议**(附件,必填 + **协议有效期**必填)
|
||||
|
||||
协议过期时:承运商展示「协议已过期」;该承运商名下车辆**不可被选择**用于安排/调整运输。
|
||||
|
||||
## 已确认业务规则
|
||||
|
||||
| 规则 | 决策 |
|
||||
|------|------|
|
||||
| 协议过期展示 | 承运商详情/列表展示 `el-tag`「协议已过期」 |
|
||||
| 协议过期选车 | 该承运商名下车辆不出现在安排/计划选车列表;后端 arrange 接口硬校验 |
|
||||
| 合作状态 | **不自动**改为「已终止」 |
|
||||
| 历史数据(无附件的旧承运商) | **B**:旧数据可保留不完整;**补全且协议有效前**不可安排运输 |
|
||||
| 调整运输(协议已过期) | **B**:承运商协议过期时**禁止任何调整运输**(须先更新协议) |
|
||||
| 附件数量 | 每类最多 5 个(对齐 CRM 客户证件) |
|
||||
| 文件类型 | JPG / PNG / PDF,单文件 ≤ 20MB |
|
||||
|
||||
> 说明:用户对「历史数据」与「调整运输」均回复 **B**;若仅指其中一项,评审时更正即可。
|
||||
|
||||
## 非目标(本期不做)
|
||||
|
||||
- 营业执照、道路经营许可证的到期日与到期提醒
|
||||
- 协议过期自动写入 `pl_document_reminder` / 提醒中心
|
||||
- 协议过期自动变更承运商合作状态
|
||||
- 复用 `pl_cert` 动态证书子表(固定三字段更简单,见方案选型)
|
||||
|
||||
---
|
||||
|
||||
## 方案选型
|
||||
|
||||
**采用方案 A:承运商表固定字段**(非 `pl_cert` 扩展)
|
||||
|
||||
| 字段 | DB 列名 | 类型 | 新建必填 | 说明 |
|
||||
|------|---------|------|----------|------|
|
||||
| 营业执照 | `business_license_urls` | varchar(2000) | ✅ | MinIO URL,逗号分隔 |
|
||||
| 道路经营许可证 | `road_transport_license_urls` | varchar(2000) | ✅ | 同上 |
|
||||
| 运输协议 | `transport_agreement_urls` | varchar(2000) | ✅ | 同上 |
|
||||
| 协议有效期 | `transport_agreement_expire_date` | date | ✅ | 仅运输协议 |
|
||||
|
||||
**计算字段(API 返回,不落库)**
|
||||
|
||||
| 字段 | 逻辑 |
|
||||
|------|------|
|
||||
| `qualificationComplete` | 三类 URL 均非空且 `transport_agreement_expire_date` 非空 |
|
||||
| `transportAgreementExpired` | `qualificationComplete` 且 `expire_date < today` |
|
||||
|
||||
---
|
||||
|
||||
## 数据模型变更
|
||||
|
||||
### `pl_carrier` 新增列(位于 `status` 与 `tenant_no` 之间)
|
||||
|
||||
```sql
|
||||
ALTER TABLE `pl_carrier`
|
||||
ADD COLUMN `business_license_urls` varchar(2000) DEFAULT NULL COMMENT '营业执照附件URL,逗号分隔' AFTER `status`,
|
||||
ADD COLUMN `road_transport_license_urls` varchar(2000) DEFAULT NULL COMMENT '道路经营许可证附件URL,逗号分隔' AFTER `business_license_urls`,
|
||||
ADD COLUMN `transport_agreement_urls` varchar(2000) DEFAULT NULL COMMENT '运输协议附件URL,逗号分隔' AFTER `road_transport_license_urls`,
|
||||
ADD COLUMN `transport_agreement_expire_date` date DEFAULT NULL COMMENT '运输协议有效期' AFTER `transport_agreement_urls`;
|
||||
```
|
||||
|
||||
同步更新:`docs/sql/pl_tables.sql`、`docs/sql/shanxia2.0.sql`。
|
||||
|
||||
---
|
||||
|
||||
## 后端设计
|
||||
|
||||
### Schema(`backend/app/schemas/pl/base_data.py`)
|
||||
|
||||
- `CarrierCreate`:四字段均必填(validator)
|
||||
- `CarrierUpdate`:四字段 optional;若传入则按新建同等校验(非空、日期合法)
|
||||
- `CarrierRead`:含四字段 + `qualificationComplete` + `transportAgreementExpired`(computed)
|
||||
|
||||
### 校验规则
|
||||
|
||||
**新建承运商**
|
||||
|
||||
- 三类 `*_urls` 去空白后非空
|
||||
- `transport_agreement_expire_date` 必填、合法 date
|
||||
|
||||
**更新承运商(历史数据 B)**
|
||||
|
||||
- 允许仅更新名称/联系人等,**不强制**一次性补全附件
|
||||
- 一旦传入任一附件/有效期字段,则按完整规则校验
|
||||
|
||||
**运输资格(安排/选车用)**
|
||||
|
||||
承运商同时满足方可参与运输:
|
||||
|
||||
```
|
||||
qualificationComplete == true
|
||||
AND transportAgreementExpired == false
|
||||
AND status == '1'(合作中)
|
||||
```
|
||||
|
||||
### API 变更
|
||||
|
||||
| 接口 | 变更 |
|
||||
|------|------|
|
||||
| `GET/POST/PUT /transport/carrier` | 读写新字段;Read 带计算字段 |
|
||||
| `GET /transport/vehicle` | 新增 query:`excludeIneligibleCarrier`(默认 false)<br>为 true 时排除:`vehicle_type=carrier` 且所属承运商不满足运输资格 |
|
||||
| `POST /transport/order/arrange/{plan_id}` | 选车后校验承运商运输资格;不通过 → 400 |
|
||||
|
||||
**arrange 错误文案(建议)**
|
||||
|
||||
- 资质不全:`该承运商资质附件不完整,无法安排运输`
|
||||
- 协议过期:`该承运商运输协议已过期,无法安排运输`
|
||||
- 调整运输且当前计划车辆所属承运商协议过期:`该承运商运输协议已过期,请先更新协议后再调整运输`(HTTP 400,禁止提交)
|
||||
|
||||
### 辅助函数
|
||||
|
||||
建议 `app/services/pl_carrier_service.py`:
|
||||
|
||||
- `carrier_qualification_complete(carrier) -> bool`
|
||||
- `carrier_agreement_expired(carrier, today: date) -> bool`
|
||||
- `carrier_eligible_for_transport(carrier, today: date) -> bool`
|
||||
- `assert_carrier_eligible_for_transport(db, carrier_id, tenant_no)` — arrange 与 vehicle 过滤共用
|
||||
|
||||
---
|
||||
|
||||
## 前端设计
|
||||
|
||||
### 1. 承运商列表 / 编辑(`views/pl/carrier/index.vue`)
|
||||
|
||||
- 弹窗宽度:`520px` → **`720px`**
|
||||
- 新增分区 **「资质附件」**(参考 CRM `CustomerFormDialog` 证件区)
|
||||
- 组件:`AttachmentUploadField` × 3
|
||||
- `accept="image/*,.pdf"`
|
||||
- `folder="pl/carrier/license"` / `permit` / `agreement`
|
||||
- `limit=5`
|
||||
- 运输协议下方:`el-date-picker type="date"`,`value-format="YYYY-MM-DD"`
|
||||
- **新建**:四字段 `formRules` 必填;提交前 `hasUploading()` 检查
|
||||
- **编辑**:同字段展示;历史不完整时可保存基础信息,但卡片/查看态标注「资质不全」
|
||||
|
||||
**卡片列表增强(建议)**
|
||||
|
||||
- `qualificationComplete === false` → `el-tag type="warning"`「资质不全」
|
||||
- `transportAgreementExpired === true` → `el-tag type="danger"`「协议已过期」
|
||||
|
||||
### 2. 查看弹窗(`pl-carrier-view-dialog`)
|
||||
|
||||
- 资质附件:可预览/下载(复用 `AttachmentUploadField` disabled 或查看态附件列表)
|
||||
- 协议有效期 + 过期 Tag
|
||||
|
||||
### 3. 安排运输(`pl-plan-arrange-dialog`)
|
||||
|
||||
- `vehicleApi.list({ vehicleStatus: 'available', excludeIneligibleCarrier: true })`
|
||||
- **调整运输 B**:打开弹窗时若当前计划已安排车辆,且其承运商 `transportAgreementExpired` 或 `!qualificationComplete`:
|
||||
- 表单项只读或禁用提交
|
||||
- 顶部 `el-alert type="warning"`:请先更新承运商资质/协议
|
||||
- 提交 arrange 前无需额外前端拦截(后端兜底),但可 preempt 提示
|
||||
|
||||
### 4. 计划表单预选车辆(`pl-plan-form-dialog`)
|
||||
|
||||
- 与安排运输相同:`excludeIneligibleCarrier: true`
|
||||
|
||||
### 5. API 类型(`api/pl/index.ts`)
|
||||
|
||||
扩展 `Carrier` 接口与 `VehicleQuery`。
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec 更新
|
||||
|
||||
更新 `openspec/changes/transport-plan-management/specs/vehicle-driver/spec.md` 中 **承运商管理**:
|
||||
|
||||
#### Scenario: 承运商资质附件维护
|
||||
|
||||
- **WHEN** 管理员新增承运商
|
||||
- **THEN** 系统 SHALL 要求上传营业执照、道路经营许可证、运输协议,并填写运输协议有效期
|
||||
- **AND** 附件 SHALL 使用 `AttachmentUploadField` 卡片化布局,URL 逗号分隔存库
|
||||
|
||||
#### Scenario: 协议过期阻断运输
|
||||
|
||||
- **WHEN** 承运商运输协议有效期早于当前日期
|
||||
- **THEN** 系统 SHALL 展示「协议已过期」
|
||||
- **AND** 该承运商名下车辆 SHALL NOT 出现在安排运输与计划预选车辆列表
|
||||
- **AND** 安排/调整运输接口 SHALL 拒绝使用该承运商车辆
|
||||
- **AND** 协议过期时 SHALL 禁止调整运输,直至更新协议
|
||||
|
||||
#### Scenario: 历史承运商兼容
|
||||
|
||||
- **WHEN** 存在未补全资质附件的历史承运商
|
||||
- **THEN** 系统 SHALL 允许保留基础档案
|
||||
- **AND** 在其资质补全且协议有效前 SHALL NOT 允许安排运输
|
||||
|
||||
---
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 层级 | 文件 |
|
||||
|------|------|
|
||||
| SQL | `docs/sql/pl_tables.sql`, `docs/sql/shanxia2.0.sql`, 迁移脚本 |
|
||||
| Model | `backend/app/models/pl_models.py` |
|
||||
| Schema | `backend/app/schemas/pl/base_data.py` |
|
||||
| Service | `backend/app/services/pl_carrier_service.py`(新建) |
|
||||
| API | `backend/app/api/pl/base_data.py`, `backend/app/api/pl/transport.py` |
|
||||
| 前端 | `views/pl/carrier/index.vue`, `pl-carrier-view-dialog`, `pl-plan-arrange-dialog`, `pl-plan-form-dialog`, `api/pl/index.ts` |
|
||||
| OpenSpec | `vehicle-driver/spec.md` |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 新建承运商:缺任一附件或协议有效期 → 前端/后端均拦截
|
||||
2. 历史承运商无附件:可编辑保存基础字段;其车辆不在安排运输选车列表
|
||||
3. 补全资质且协议未过期:车辆恢复可选
|
||||
4. 协议过期:承运商展示「协议已过期」;车辆不可选;arrange 返回 400
|
||||
5. 已安排计划 + 承运商后过期:打开「调整运输」不可提交,提示先更新协议
|
||||
6. 自有车辆(`vehicle_type=self`)不受承运商规则影响
|
||||
7. 查看弹窗可预览/下载三类附件
|
||||
|
||||
---
|
||||
|
||||
## 工作量估算
|
||||
|
||||
约 **2 人日**(含 DDL、CRUD、表单/查看、选车过滤、arrange 校验、联调)。
|
||||
@@ -0,0 +1,382 @@
|
||||
# 运输计划 · 车辆 ETC/加油充值申请与发放 — 设计文档
|
||||
|
||||
> **日期**: 2026-06-29
|
||||
> **状态**: 已批准
|
||||
> **前置**: `docs/sql/pl_tables.sql`(`pl_vehicle`)、`docs/superpowers/specs/2026-06-25-pl-freight-settlement-design.md`(申请→登记两阶段参考)
|
||||
> **界面模式**: P01 标准列表 CRUD(双菜单)+ P13 财务登记式表单(发放页)
|
||||
|
||||
## 目标
|
||||
|
||||
在运输管理模块新增 **ETC/加油充值** 管理能力,支持:
|
||||
|
||||
1. **【ETC加油申请】**:业务人员提交充值申请(车牌、类型、申请日期、期初公里数、附件、申请人)。
|
||||
2. **【ETC加油发放登记】**:财务/行政登记实际充值结果(订单号、充值日期、卡号、加油站、金额、附件、关联申请单)。
|
||||
|
||||
两菜单独立,职责分离;**本期不做审批流**——申请提交后直接进入「待发放」,由登记人在发放页关联并完成登记。
|
||||
|
||||
## 已确认业务规则
|
||||
|
||||
| # | 规则 | 决策 |
|
||||
|---|------|------|
|
||||
| D1 | 审批 | **A:无需审批**;申请提交即「待发放」 |
|
||||
| D2 | 菜单结构 | **两个独立菜单**:ETC加油申请 / ETC加油发放登记 |
|
||||
| D3 | 申请与发放关联 | **1 申请 : 0~1 发放**;已发放的申请不可再关联 |
|
||||
| D4 | 无申请补录 | **允许**直接新建发放登记(历史数据补录),`apply_id` 可空 |
|
||||
| D5 | 车牌范围 | 下拉取自 **`pl_vehicle` 全量**(自有 + 承运商车辆) |
|
||||
| D6 | 类型一致性 | 发放关联申请时,**自动带出**车牌号、充值类型且只读 |
|
||||
| D7 | 申请人/创建人 | 默认当前登录用户,表单只读展示;落库 `applicant_id` / `create_by` |
|
||||
| D8 | 附件 | JPG/PNG/PDF,单文件 ≤ 20MB,每单最多 5 个;MinIO 逗号分隔 URL |
|
||||
| D9 | 充值订单号 | 后端自动生成:`SXCL` + `YYYYMMDD` + 6 位序号,例 `SXCL20260628000001` |
|
||||
| D10 | 申请单号 | 后端自动生成:`SXSQ` + `YYYYMMDD` + 6 位序号(便于列表检索与关联) |
|
||||
| D11 | 加油站 | 下拉预设 + 自由填写(`el-select` + `allow-create`);预设:中石化、线上etc-app、百链 |
|
||||
| D12 | 删除 | 软删除;**已关联发放记录的申请不可删除**;已有 `apply_id` 关联的发放记录删除时释放申请状态 |
|
||||
|
||||
## 非目标(本期不做)
|
||||
|
||||
- 接入通用工作流 / 主管审批
|
||||
- ETC 卡、油卡主数据维护(卡号每次手填)
|
||||
- 充值报表导出、统计看板
|
||||
- 与运输单 / 地磅 / 计划自动联动
|
||||
- 小程序端申请
|
||||
- 一次申请多次分批发放
|
||||
|
||||
---
|
||||
|
||||
## 方案选型
|
||||
|
||||
**采用方案 A:两个独立菜单**(非单 Tab、非单表状态机)
|
||||
|
||||
| 方案 | 说明 | 结论 |
|
||||
|------|------|------|
|
||||
| A 双菜单 | 申请与发放分页面、分权限 | ✅ 采用 |
|
||||
| B 单菜单 Tab | 申请 \| 待发放 \| 已发放 | 权限边界模糊 |
|
||||
| C 单表状态 | 一张表混放申请/发放字段 | 扩展性差 |
|
||||
|
||||
---
|
||||
|
||||
## 页面与路由
|
||||
|
||||
| 菜单名 | Hash | 组件 | menuCode |
|
||||
|--------|------|------|----------|
|
||||
| ETC加油申请 | `/#/pl/vehicle-recharge-apply` | `web-admin/src/views/pl/vehicle-recharge-apply/index.vue` | `pl:vehicle-recharge-apply:list` |
|
||||
| ETC加油发放登记 | `/#/pl/vehicle-recharge-record` | `web-admin/src/views/pl/vehicle-recharge-record/index.vue` | `pl:vehicle-recharge-record:list` |
|
||||
|
||||
**侧栏位置**:运输管理 → 与「车辆列表」「山峡自有车辆」「运费结算」并列。
|
||||
|
||||
### 权限编码
|
||||
|
||||
| 模块 | 权限 |
|
||||
|------|------|
|
||||
| 申请 | `pl:vehicle-recharge-apply:list` `query` `add` `edit` `remove` |
|
||||
| 发放 | `pl:vehicle-recharge-record:list` `query` `add` `edit` `remove` |
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 1. 申请单 `pl_vehicle_recharge_apply`
|
||||
|
||||
```sql
|
||||
CREATE TABLE `pl_vehicle_recharge_apply` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`apply_no` varchar(32) NOT NULL COMMENT '申请单号 SXSQ+YYYYMMDD+6位',
|
||||
`license_plate` varchar(20) NOT NULL COMMENT '车牌号',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`apply_date` date NOT NULL COMMENT '申请日期',
|
||||
`odometer_km` decimal(10,1) DEFAULT NULL COMMENT '期初公里数',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`applicant_id` varchar(64) NOT NULL COMMENT '申请人用户ID',
|
||||
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending-待发放 disbursed-已发放 cancelled-已作废',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_apply_no` (`apply_no`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_license_plate` (`license_plate`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_apply_date` (`apply_date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值申请';
|
||||
```
|
||||
|
||||
### 2. 发放登记 `pl_vehicle_recharge_record`
|
||||
|
||||
```sql
|
||||
CREATE TABLE `pl_vehicle_recharge_record` (
|
||||
`id` varchar(64) NOT NULL COMMENT '主键ID',
|
||||
`recharge_order_no` varchar(32) NOT NULL COMMENT '充值订单号 SXCL+YYYYMMDD+6位',
|
||||
`recharge_date` date NOT NULL COMMENT '充值日期',
|
||||
`recharge_type` varchar(20) NOT NULL COMMENT '充值类型 etc-ETC fuel-加油',
|
||||
`card_no` varchar(64) DEFAULT NULL COMMENT '卡号',
|
||||
`gas_station` varchar(200) DEFAULT NULL COMMENT '加油站/充值渠道',
|
||||
`amount` decimal(10,2) NOT NULL COMMENT '充值金额(元)',
|
||||
`attachment_urls` varchar(2000) DEFAULT NULL COMMENT '附件URL,逗号分隔',
|
||||
`apply_id` varchar(64) DEFAULT NULL COMMENT '关联申请单ID',
|
||||
`license_plate` varchar(20) DEFAULT NULL COMMENT '车牌号(冗余,便于无关联申请时列表展示)',
|
||||
`tenant_no` varchar(64) NOT NULL DEFAULT 'shanxia2.0' COMMENT '租户编号',
|
||||
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
|
||||
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
|
||||
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
|
||||
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`del_flag` smallint(6) NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_recharge_order_no` (`recharge_order_no`),
|
||||
UNIQUE KEY `uk_apply_id` (`apply_id`),
|
||||
KEY `idx_tenant` (`tenant_no`),
|
||||
KEY `idx_recharge_date` (`recharge_date`),
|
||||
KEY `idx_license_plate` (`license_plate`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆ETC/加油充值发放登记';
|
||||
```
|
||||
|
||||
> `uk_apply_id`:保证同一申请最多一条有效发放记录(MySQL 允许多个 NULL)。
|
||||
|
||||
**DDL 同步**:`docs/sql/pl_tables.sql`、`docs/sql/shanxia2.0.sql`、迁移脚本 `docs/sql/migrations/2026-06-29-pl-vehicle-recharge.sql`。
|
||||
|
||||
### 3. 字典
|
||||
|
||||
| dict_type | 标签 | 值 | Tag 色 |
|
||||
|-----------|------|-----|--------|
|
||||
| `pl_recharge_type` | ETC | `etc` | primary |
|
||||
| `pl_recharge_type` | 加油 | `fuel` | success |
|
||||
| `pl_recharge_apply_status` | 待发放 | `pending` | warning |
|
||||
| `pl_recharge_apply_status` | 已发放 | `disbursed` | success |
|
||||
| `pl_recharge_apply_status` | 已作废 | `cancelled` | info |
|
||||
|
||||
加油站预设写入前端常量 + 可选 dict `pl_gas_station`(中石化 / 线上etc-app / 百链);自由填写值仅存 `gas_station` 字段,本期不入字典。
|
||||
|
||||
---
|
||||
|
||||
## 状态机
|
||||
|
||||
```
|
||||
申请创建 ──→ pending(待发放)
|
||||
├── 关联发放登记成功 ──→ disbursed(已发放)
|
||||
└── 手动作废 ──→ cancelled(已作废)
|
||||
|
||||
发放登记创建(含 apply_id)──→ 申请 status 更新为 disbursed
|
||||
发放登记删除(软删)──→ 若曾关联申请,申请回退为 pending
|
||||
```
|
||||
|
||||
| 操作 | 规则 |
|
||||
|------|------|
|
||||
| 编辑申请 | 仅 `pending` 可编辑 |
|
||||
| 作废申请 | 仅 `pending`;手动作废,不物理删除 |
|
||||
| 删除申请 | 仅 `pending` 且无关联发放 |
|
||||
| 新建发放 | 选关联申请时,申请须为 `pending` |
|
||||
| 编辑发放 | 允许;若改 `apply_id` 须重新校验 |
|
||||
| 删除发放 | 软删;释放原申请为 `pending` |
|
||||
|
||||
---
|
||||
|
||||
## 单号生成
|
||||
|
||||
对齐 `backend/app/api/pl/plan.py` `_generate_plan_no`:
|
||||
|
||||
```python
|
||||
def _generate_apply_no(db: Session) -> str:
|
||||
prefix = f"SXSQ{date.today():%Y%m%d}"
|
||||
# 查最大序号(uk 含软删,不过滤 del_flag)
|
||||
...
|
||||
|
||||
def _generate_recharge_order_no(db: Session) -> str:
|
||||
prefix = f"SXCL{date.today():%Y%m%d}"
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端设计
|
||||
|
||||
### 模块位置
|
||||
|
||||
| 层 | 路径 |
|
||||
|----|------|
|
||||
| Model | `backend/app/models/pl_models.py` |
|
||||
| Schema | `backend/app/schemas/pl/vehicle_recharge.py`(新建) |
|
||||
| CRUD | `backend/app/crud/pl/vehicle_recharge_apply.py`、`vehicle_recharge_record.py` |
|
||||
| API | `backend/app/api/pl/vehicle_recharge.py` |
|
||||
| 注册 | `app/api/__init__.py`、`app/main.py` |
|
||||
|
||||
### API 清单
|
||||
|
||||
**申请** `/api/admin/pl/vehicle-recharge-apply`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/list` | 分页列表;筛选:q(单号/车牌)、rechargeType、status、applyDateFrom/To、applicantId |
|
||||
| GET | `/{id}` | 详情 |
|
||||
| POST | `/` | 新建;生成 apply_no;status=pending |
|
||||
| PUT | `/{id}` | 更新;仅 pending |
|
||||
| DELETE | `/{ids}` | 软删;仅 pending |
|
||||
| POST | `/{id}/cancel` | 作废 → cancelled |
|
||||
| GET | `/pending-options` | 待发放申请下拉(供发放页关联;返回 id、applyNo、licensePlate、rechargeType) |
|
||||
|
||||
**发放** `/api/admin/pl/vehicle-recharge-record`
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/list` | 分页;筛选:q(订单号/车牌/卡号)、rechargeType、rechargeDateFrom/To、gasStation、applyNo |
|
||||
| GET | `/{id}` | 详情(含关联申请摘要) |
|
||||
| POST | `/` | 新建;生成 recharge_order_no;若带 apply_id 则更新申请为 disbursed |
|
||||
| PUT | `/{id}` | 更新 |
|
||||
| DELETE | `/{ids}` | 软删;释放关联申请 |
|
||||
|
||||
### 校验要点
|
||||
|
||||
- `license_plate` 须存在于当前租户 `pl_vehicle`(del_flag=0)
|
||||
- `recharge_type` ∈ `{etc, fuel}`
|
||||
- `odometer_km` ≥ 0,最多 1 位小数
|
||||
- `amount` > 0,最多 2 位小数
|
||||
- 新建/更新发放时:若 `apply_id` 非空,申请须 `pending`,且类型一致;自动写入 `license_plate`
|
||||
- 无 `apply_id` 时:`license_plate` 表单必填(补录场景)
|
||||
- 租户隔离 + 数据权限(沿用 PL 模块惯例)
|
||||
|
||||
### `_row_to_dict` 可读字段(申请)
|
||||
|
||||
`id`, `applyNo`, `licensePlate`, `rechargeType`, `applyDate`, `odometerKm`, `attachmentUrls`, `applicantId`, `applicantName`(computed), `status`, `remark`, `createTime`, `updateTime`
|
||||
|
||||
### `_row_to_dict` 可读字段(发放)
|
||||
|
||||
`id`, `rechargeOrderNo`, `rechargeDate`, `rechargeType`, `cardNo`, `gasStation`, `amount`, `attachmentUrls`, `applyId`, `applyNo`(computed), `licensePlate`, `remark`, `createBy`, `createByName`(computed), `createTime`, `updateTime`
|
||||
|
||||
---
|
||||
|
||||
## 前端设计
|
||||
|
||||
### 1. ETC加油申请(P01)
|
||||
|
||||
**参考**:`views/system/unit/index.vue` + `views/pl/carrier/index.vue`(附件)
|
||||
|
||||
**筛选**:关键词(申请单号/车牌)、充值类型、状态、申请日期范围、申请人
|
||||
|
||||
**列表列**:申请单号、车牌、充值类型(Tag)、申请日期、期初公里数、申请人、状态(Tag)、创建时间、操作(查看/编辑/作废/删除)
|
||||
|
||||
**工具栏**:已选 N 项 → 新增申请(primary)
|
||||
|
||||
**新增/编辑弹窗**(`dialog-form-layout`,720px,`form-grid-2`):
|
||||
|
||||
| 字段 | 控件 |
|
||||
|------|------|
|
||||
| 申请单号 | 编辑/查看只读;新建保存后展示 |
|
||||
| 车牌号 | `el-select` filterable,选项来自 `vehicleApi.list` |
|
||||
| 充值类型 | `el-select` + dict |
|
||||
| 申请日期 | `el-date-picker` `YYYY-MM-DD` |
|
||||
| 期初公里数 | `el-input-number` min=0 precision=1 |
|
||||
| 附件 | `AttachmentUploadField` folder=`pl/vehicle-recharge/apply` limit=5 |
|
||||
| 申请人 | 只读,当前用户姓名 |
|
||||
| 备注 | textarea 跨列 |
|
||||
|
||||
**查看弹窗**:`SystemViewDialog` 或专用 view dialog,附件可预览/下载。
|
||||
|
||||
**行操作**:
|
||||
|
||||
- `pending`:查看、编辑、作废、删除
|
||||
- `disbursed`:仅查看
|
||||
- `cancelled`:仅查看、删除(可选,默认保留只读)
|
||||
|
||||
### 2. ETC加油发放登记(P01 + P13)
|
||||
|
||||
**参考**:`views/pl/freight-disburse` 登记思路、`AttachmentUploadField`
|
||||
|
||||
**筛选**:关键词(订单号/车牌/卡号)、充值类型、充值日期范围、加油站、关联申请单号
|
||||
|
||||
**列表列**:充值订单号、充值日期、充值类型(Tag)、车牌、卡号、加油站、金额、关联申请单号、创建人、操作
|
||||
|
||||
**新增/编辑弹窗**(720px):
|
||||
|
||||
| 字段 | 控件 |
|
||||
|------|------|
|
||||
| 充值订单号 | 只读(新建提示「保存后自动生成」) |
|
||||
| 关联申请单 | `el-select` clearable,remote/本地加载 `pending-options`;选中后带出车牌、类型只读 |
|
||||
| 车牌号 | 有关联时只读;无关联时必选 |
|
||||
| 充值类型 | 有关联时只读;无关联必选 |
|
||||
| 充值日期 | date picker |
|
||||
| 卡号 | text input |
|
||||
| 加油站 | `el-select` allow-create filterable,预设三项 |
|
||||
| 充值金额 | input-number precision=2 min=0.01 |
|
||||
| 附件 | `AttachmentUploadField` folder=`pl/vehicle-recharge/record` |
|
||||
| 备注 | textarea |
|
||||
| 创建人 | 只读 |
|
||||
|
||||
**快捷入口(可选增强)**:申请列表行操作增加「去登记」→ 打开发放新建弹窗并预填 `applyId`(二期可做,本期发放页选手动关联即可)。
|
||||
|
||||
### 3. API 类型(`web-admin/src/api/pl/index.ts`)
|
||||
|
||||
新增 `VehicleRechargeApply`、`VehicleRechargeRecord` 接口与 CRUD 方法。
|
||||
|
||||
### 4. UI 规范
|
||||
|
||||
- 状态/类型字段用 `el-tag` + dict Store
|
||||
- 按钮带 `@element-plus/icons-vue` 图标
|
||||
- 弹窗 `align-center`,无 backdrop blur
|
||||
- 操作列 `fixed="right"` min-width ≥ 260px
|
||||
- 列宽用 `min-width`
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec 更新(建议)
|
||||
|
||||
在 `openspec/changes/transport-plan-management/` 下新增 capability `vehicle-recharge/spec.md`,或扩展 `vehicle-driver/spec.md`:
|
||||
|
||||
#### Scenario: ETC加油申请提交
|
||||
|
||||
- **WHEN** 用户提交充值申请
|
||||
- **THEN** 系统 SHALL 生成申请单号并置状态为待发放
|
||||
- **AND** 申请人 SHALL 默认为当前登录用户
|
||||
|
||||
#### Scenario: 发放登记关联申请
|
||||
|
||||
- **WHEN** 财务创建发放登记并关联待发放申请
|
||||
- **THEN** 系统 SHALL 自动生成充值订单号 `SXCL+日期+序号`
|
||||
- **AND** 申请状态 SHALL 更新为已发放
|
||||
- **AND** 同一申请 SHALL NOT 被重复关联
|
||||
|
||||
#### Scenario: 无申请补录
|
||||
|
||||
- **WHEN** 用户创建发放登记且不关联申请
|
||||
- **THEN** 系统 SHALL 允许保存并要求手动填写车牌与充值类型
|
||||
|
||||
---
|
||||
|
||||
## 影响范围
|
||||
|
||||
| 层级 | 文件 |
|
||||
|------|------|
|
||||
| SQL | `pl_tables.sql`, `shanxia2.0.sql`, migration |
|
||||
| Backend | `pl_models.py`, schemas, crud, api, `__init__.py`, `main.py` |
|
||||
| Frontend | 两页面 vue、api/pl、dict 注册 |
|
||||
| 菜单 | 迁移 SQL 或菜单管理脚本 |
|
||||
| OpenSpec | transport-plan-management spec(可选) |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 新建申请:必填校验通过 → 生成 `SXSQ…` 单号,状态「待发放」,申请人为当前用户
|
||||
2. 待发放申请可在发放页关联;关联后申请变「已发放」,不可再选
|
||||
3. 新建发放:生成 `SXCL…` 订单号;金额、日期、类型校验生效
|
||||
4. 无关联申请的发放:可手填车牌、类型并保存
|
||||
5. 删除发放:关联申请回退「待发放」
|
||||
6. 已发放申请不可编辑/删除;待发放可编辑
|
||||
7. 附件上传/预览/下载正常;列表筛选、分页、权限按钮生效
|
||||
8. 租户隔离:跨租户数据不可见
|
||||
|
||||
---
|
||||
|
||||
## 工作量估算
|
||||
|
||||
| 项 | 估时 |
|
||||
|----|------|
|
||||
| DDL + Model + CRUD + API + 单号/状态联动 | 1~1.5 人日 |
|
||||
| 前端双页 + API + 字典 + 附件 | 1.5~2 人日 |
|
||||
| 菜单权限 + 联调 + field consistency 脚本 | 0.5 人日 |
|
||||
| **合计** | **约 3~4 人日** |
|
||||
@@ -0,0 +1,167 @@
|
||||
# 运输计划 · 车辆维保申请与处理登记 — 设计文档
|
||||
|
||||
> **日期**: 2026-06-29
|
||||
> **状态**: 已批准
|
||||
> **前置**: `docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md`(申请→登记两阶段参考)、`docs/sql/pl_tables.sql`(`pl_vehicle`)
|
||||
> **界面模式**: P01 标准列表 CRUD(双菜单);处理登记页参考 ETC 发放看板(申请 + 处理合并列表)
|
||||
|
||||
## 目标
|
||||
|
||||
在运输管理模块新增 **车辆维保** 管理能力,支持:
|
||||
|
||||
1. **【车辆维保申请】**:提交维保申请(车牌、申请人、申请时间、维保类型、开始/取车时间、内容、店铺地址、附件、备注)。
|
||||
2. **【车辆维保处理登记】**:登记处理结果(金额、处理人、处理时间、附件;可关联申请或无申请补录)。
|
||||
|
||||
两菜单独立;**本期不做审批流**——申请提交后进入「待处理」,登记处理完成后变为「已处理」。
|
||||
|
||||
## 已确认业务规则
|
||||
|
||||
| # | 规则 | 决策 |
|
||||
|---|------|------|
|
||||
| D1 | 审批 | **无需审批**;申请提交即「待处理」 |
|
||||
| D2 | 菜单结构 | **车辆维保申请** / **车辆维保处理登记** |
|
||||
| D3 | 申请与处理关联 | **1 申请 : 0~1 处理**;已处理的申请不可再关联 |
|
||||
| D4 | 无申请补录 | **允许**;`apply_id` 可空 |
|
||||
| D5 | 处理完成语义 | 提交处理登记即 **已处理**;不设独立「是否已处理」字段 |
|
||||
| D6 | 金额 | **非必填**;有值须 > 0;可后补编辑 |
|
||||
| D7 | 申请人/处理人 | 下拉选择用户(`fetchUserOptions`);默认当前登录人 |
|
||||
| D8 | 维保类型 | 下拉预设(保养、维修、年检)+ **allow-create 自定义**;存 varchar 自由文本 |
|
||||
| D9 | 车辆状态联动 | **V1 不自动**改 `pl_vehicle.vehicle_status` |
|
||||
| D10 | 选车提醒 | 见 §选车维保提醒 |
|
||||
| D11 | 申请单号 | `SXWB` + `YYYYMMDD` + 6 位序号 |
|
||||
| D12 | 处理单号 | `SXWC` + `YYYYMMDD` + 6 位序号 |
|
||||
| D13 | 附件 | JPG/PNG/PDF,单文件 ≤ 20MB,每单最多 5 个;MinIO 逗号分隔 URL |
|
||||
| D14 | 删除 | 软删除;已关联处理记录的申请不可删;删处理记录时释放申请为待处理 |
|
||||
|
||||
## 非目标(本期不做)
|
||||
|
||||
- 接入通用工作流 / 主管审批
|
||||
- 维保店铺主数据维护
|
||||
- 申请后自动将车辆设为「维修中」
|
||||
- 维保费用统计报表、导出
|
||||
- 小程序端申请
|
||||
- 一次申请多次分批处理
|
||||
|
||||
---
|
||||
|
||||
## 选车维保提醒
|
||||
|
||||
在申请页(及处理页补录选车牌时)选择车牌后:
|
||||
|
||||
**触发条件**(同时满足):
|
||||
|
||||
1. 存在该车牌 **待处理**(`status=pending`)的维保申请;
|
||||
2. `expected_pickup_date IS NULL` **或** `today <= expected_pickup_date`。
|
||||
|
||||
**不提醒**:`today > expected_pickup_date`(即使仍为待处理)。
|
||||
|
||||
**展示**:`ElMessageBox.alert` 或等价弹窗,展示维保类型、开始时间、预计取车、内容、店铺地址、申请单号等;多条时合并展示或取最近一条(实现取 **全部待提醒** 列表)。
|
||||
|
||||
**API**:`GET /api/transport/vehicle-maintenance-apply/reminders?licensePlate=xxx`
|
||||
|
||||
---
|
||||
|
||||
## 页面与路由
|
||||
|
||||
| 菜单名 | Hash | 组件 | menuCode |
|
||||
|--------|------|------|----------|
|
||||
| 车辆维保申请 | `/#/pl/vehicle-maintenance-apply` | `web-admin/src/views/pl/vehicle-maintenance-apply/index.vue` | `pl:vehicle-maintenance-apply:list` |
|
||||
| 车辆维保处理登记 | `/#/pl/vehicle-maintenance-process` | `web-admin/src/views/pl/vehicle-maintenance-process/index.vue` | `pl:vehicle-maintenance-process:list` |
|
||||
|
||||
**侧栏位置**:运输管理 → 与 ETC/加油 菜单并列(`order_num` 33 / 34)。
|
||||
|
||||
### 权限编码
|
||||
|
||||
| 模块 | 权限 |
|
||||
|------|------|
|
||||
| 申请 | `pl:vehicle-maintenance-apply:list` `query` `add` `edit` `remove` `cancel` |
|
||||
| 处理 | `pl:vehicle-maintenance-process:list` `query` `add` `edit` `remove` |
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 1. 申请单 `pl_vehicle_maintenance_apply`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | varchar(64) | UUID 主键 |
|
||||
| apply_no | varchar(32) | SXWB+日期+6位,唯一 |
|
||||
| license_plate | varchar(20) | 车牌,校验 pl_vehicle |
|
||||
| applicant_id | varchar(64) | 申请人 |
|
||||
| apply_date | date | 申请时间 |
|
||||
| maintenance_type | varchar(64) | 维保类型(可自定义文本) |
|
||||
| start_date | date | 维保开始时间 |
|
||||
| expected_pickup_date | date NULL | 预计取车时间 |
|
||||
| maintenance_content | varchar(1000) | 维保内容 |
|
||||
| shop_address | varchar(500) | 维保店铺地址 |
|
||||
| attachment_urls | varchar(2000) | 申请附件 |
|
||||
| status | varchar(20) | pending / processed / cancelled |
|
||||
| + 公共字段 | | tenant_no … del_flag |
|
||||
|
||||
**日期校验**:`apply_date <= start_date`;若 `expected_pickup_date` 有值则 `start_date <= expected_pickup_date`。
|
||||
|
||||
### 2. 处理登记 `pl_vehicle_maintenance_process`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | varchar(64) | UUID 主键 |
|
||||
| process_no | varchar(32) | SXWC+日期+6位,唯一 |
|
||||
| apply_id | varchar(64) NULL | 关联申请,唯一索引(允许多 NULL) |
|
||||
| license_plate | varchar(20) | 车牌冗余 |
|
||||
| amount | decimal(10,2) NULL | 金额(元) |
|
||||
| handler_id | varchar(64) | 处理人 |
|
||||
| process_date | date | 处理时间 |
|
||||
| attachment_urls | varchar(2000) | 处理附件 |
|
||||
| + 公共字段 | | |
|
||||
|
||||
### 字典
|
||||
|
||||
| dict_type | 预设 |
|
||||
|-----------|------|
|
||||
| pl_maintenance_type | 保养、维修、年检 |
|
||||
| pl_maintenance_apply_status | 待处理、已处理、已作废 |
|
||||
|
||||
---
|
||||
|
||||
## 状态流转
|
||||
|
||||
```
|
||||
创建申请 → pending
|
||||
pending + 登记处理(有 apply_id)→ processed
|
||||
pending → cancelled(作废)
|
||||
删除处理记录 → 关联申请回 pending
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 概要
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/transport/vehicle-maintenance-apply/` | 申请分页列表 |
|
||||
| POST | `/transport/vehicle-maintenance-apply/` | 新建申请 |
|
||||
| PUT | `/transport/vehicle-maintenance-apply/{id}` | 编辑(仅 pending) |
|
||||
| POST | `/transport/vehicle-maintenance-apply/{id}/cancel` | 作废 |
|
||||
| DELETE | `/transport/vehicle-maintenance-apply/{id}` | 软删 |
|
||||
| GET | `/transport/vehicle-maintenance-apply/reminders` | 选车提醒 |
|
||||
| GET | `/transport/vehicle-maintenance-process/process-board` | 处理看板(申请 LEFT JOIN 处理) |
|
||||
| POST | `/transport/vehicle-maintenance-process/` | 新建处理(含补录) |
|
||||
| PUT | `/transport/vehicle-maintenance-process/{id}` | 编辑处理 |
|
||||
| DELETE | `/transport/vehicle-maintenance-process/{id}` | 软删处理 |
|
||||
|
||||
---
|
||||
|
||||
## 实现参考
|
||||
|
||||
复制并改造 ETC/加油 模块:
|
||||
|
||||
- 后端:`pl_vehicle_recharge_service.py` → `pl_vehicle_maintenance_service.py`
|
||||
- 前端:`vehicle-recharge-apply` / `vehicle-recharge-record` → 维保对应页面
|
||||
- 静态路由:`web-admin/src/router/modules/pl.ts`
|
||||
|
||||
---
|
||||
|
||||
## OpenSpec
|
||||
|
||||
变更目录:`openspec/changes/vehicle-maintenance-management/`
|
||||
@@ -0,0 +1,115 @@
|
||||
# 运输计划 · 车辆尿素申请与发放登记 — 设计文档
|
||||
|
||||
> **日期**: 2026-06-29
|
||||
> **状态**: 已批准
|
||||
> **前置**: `docs/superpowers/specs/2026-06-29-pl-vehicle-recharge-design.md`、`docs/superpowers/specs/2026-06-29-vehicle-maintenance-design.md`
|
||||
> **界面模式**: P01 标准列表 CRUD(双菜单);发放页参考 ETC 发放看板
|
||||
|
||||
## 目标
|
||||
|
||||
在运输管理模块新增 **车辆尿素** 管理能力:
|
||||
|
||||
1. **【车辆尿素申请】**:提交入库/采购类申请(车牌、申请人、申请时间、入库时间、单位、入库数、申请金额、附件、备注)。
|
||||
2. **【车辆尿素发放登记】**:登记实际发放(发放金额、处理人、发放时间、附件;可关联申请或无申请补录)。
|
||||
|
||||
无审批流;申请提交后 **待发放**,发放登记后 **已发放**。
|
||||
|
||||
## 已确认业务规则
|
||||
|
||||
| # | 规则 | 决策 |
|
||||
|---|------|------|
|
||||
| D1 | 审批 | **无需审批** |
|
||||
| D2 | 菜单 | **车辆尿素申请** / **车辆尿素发放登记** |
|
||||
| D3 | 关联 | **1 申请 : 0~1 发放** |
|
||||
| D4 | 无申请补录 | **允许** |
|
||||
| D5 | 申请 vs 发放金额 | **计划 vs 实付**;发放默认带出申请金额,可修改 |
|
||||
| D6 | 申请金额 | **必填** > 0 |
|
||||
| D7 | 发放金额 | **必填** > 0 |
|
||||
| D8 | 入库数 | **必填**,支持小数 |
|
||||
| D9 | 单位 | 默认「吨桶」+ **allow-create** |
|
||||
| D10 | 是否已处理 | **不设字段**;发放提交 → `disbursed` |
|
||||
| D11 | 申请人/处理人 | 用户下拉,默认当前登录人 |
|
||||
| D12 | 单号 | 申请 `SXNS*` / 发放 `SXUF*` |
|
||||
| D13 | 日期 | `apply_date ≤ warehouse_in_date` |
|
||||
| D14 | 附件 | JPG/PNG/PDF,≤20MB,最多 5 个 |
|
||||
| D15 | 删除 | 软删除;有关联发放的申请不可删;删发放释放申请为待发放 |
|
||||
|
||||
## 非目标(V1)
|
||||
|
||||
- 审批流、库存台账联动、报表导出、小程序、车辆状态联动、选车提醒
|
||||
|
||||
---
|
||||
|
||||
## 页面与路由
|
||||
|
||||
| 菜单 | Hash | menuCode |
|
||||
|------|------|----------|
|
||||
| 车辆尿素申请 | `/#/pl/vehicle-urea-apply` | `pl:vehicle-urea-apply:list` |
|
||||
| 车辆尿素发放登记 | `/#/pl/vehicle-urea-disburse` | `pl:vehicle-urea-disburse:list` |
|
||||
|
||||
侧栏 `order_num`: 35 / 36(运输管理下,维保之后)。
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### `pl_vehicle_urea_apply`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| apply_no | varchar(32) | SXNS+日期+6位 |
|
||||
| license_plate | varchar(20) | 车牌 |
|
||||
| applicant_id | varchar(64) | 申请人 |
|
||||
| apply_date | date | 申请时间 |
|
||||
| warehouse_in_date | date | 入库时间 |
|
||||
| unit | varchar(32) | 单位 |
|
||||
| quantity | decimal(10,2) | 入库数 |
|
||||
| apply_amount | decimal(10,2) | 申请金额 |
|
||||
| attachment_urls | varchar(2000) | 附件 |
|
||||
| status | varchar(20) | pending / disbursed / cancelled |
|
||||
|
||||
### `pl_vehicle_urea_disburse`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| disburse_no | varchar(32) | SXUF+日期+6位 |
|
||||
| apply_id | varchar(64) NULL | 关联申请,唯一 |
|
||||
| license_plate | varchar(20) | 冗余 |
|
||||
| amount | decimal(10,2) | 发放金额(实付) |
|
||||
| handler_id | varchar(64) | 处理人 |
|
||||
| disburse_date | date | 发放时间 |
|
||||
| attachment_urls | varchar(2000) | 附件 |
|
||||
|
||||
### 字典
|
||||
|
||||
- `pl_urea_unit`:吨桶(UI 预设 + allow-create,库内可存自由文本)
|
||||
- `pl_urea_apply_status`:待发放 / 已发放 / 已作废
|
||||
|
||||
---
|
||||
|
||||
## API 概要
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `/transport/vehicle-urea-apply/*` | 申请 CRUD + 作废 |
|
||||
| `/transport/vehicle-urea-disburse/disburse-board` | 发放看板 |
|
||||
| `/transport/vehicle-urea-disburse/*` | 发放 CRUD |
|
||||
|
||||
---
|
||||
|
||||
## 发放弹窗交互
|
||||
|
||||
- 只读展示:车牌、入库时间、单位、入库数、**申请金额**
|
||||
- **发放金额**:默认 = 申请金额,可编辑(实付可与计划不同)
|
||||
- **处理人**:下拉,默认当前用户
|
||||
- **发放时间**:默认今天
|
||||
|
||||
---
|
||||
|
||||
## 实现参考
|
||||
|
||||
- 后端/页面:复制 `pl_vehicle_recharge_*` / `vehicle-recharge-*`
|
||||
- 申请扩展字段:入库时间、单位、数量、申请金额
|
||||
- `sys_role_menu`:**固定 id / MD5**,禁止单语句多行 `UUID()`
|
||||
|
||||
OpenSpec:`openspec/changes/vehicle-urea-management/`
|
||||
Reference in new issue
Block a user