feat: 新增电工APP端与定时任务管理功能

- 新增电工APP独立鉴权中间件与登录注册接口
- 新增定时任务管理模块,支持任务配置、执行日志与手动触发
- 新增运维费定价与电工派单成本价目管理
- 新增电工资质证照管理与到期提醒定时任务
- 完善数据库实体与DTO校验,优化权限控制
This commit is contained in:
andyesfly committed 2026-10-02 16:08:00 +08:00
1 parent 1803ad679c
commit f4454ad014
57 files changed
+9388 -200

No files matched your search

@@ -0,0 +1,221 @@
-- 运维费定价 + 电工登记管理功能扩展
-- 背景:运营派单域由「派单执行」扩展为「客户运维服务价目 + 电工派单成本价 + 电工登记审核」,
-- 对应新增 service_price(客户运维服务价目)、worker_cert(电工资质证照)、
-- sys_notice(证照到期站内通知)三张表,并为 worker / dispatch_price 补齐
-- 登录、审核、计价方式等业务字段,同时挂载运营派单子菜单与按钮权限。
-- 幂等:新增列一律带 DEFAULT + IF NOT EXISTS;建表/索引用 IF NOT EXISTS;
-- 菜单/角色绑定用 ON CONFLICT;字典用「先 DELETE 再 INSERT」收敛。
-- 注意:本文件不 TRUNCATE sys_dict(避免清空其它字典及重置 id 影响 db_seed.sql)。
BEGIN;
-- ------------------------------------------------------------
-- 1) worker 表扩展:电工 APP 登录与入驻审核
-- ------------------------------------------------------------
ALTER TABLE worker ADD COLUMN IF NOT EXISTS password_hash VARCHAR(255); -- 电工APP登录密码bcrypt哈希
ALTER TABLE worker ADD COLUMN IF NOT EXISTS audit_status SMALLINT NOT NULL DEFAULT 0; -- 审核状态:0待审核 1已通过 2已驳回
ALTER TABLE worker ADD COLUMN IF NOT EXISTS audit_remark VARCHAR(255); -- 驳回理由/审核备注
ALTER TABLE worker ADD COLUMN IF NOT EXISTS audit_by BIGINT; -- 审核人(→ sys_user.id)
ALTER TABLE worker ADD COLUMN IF NOT EXISTS audit_at TIMESTAMPTZ; -- 审核时间
ALTER TABLE worker ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ; -- 最后登录时间
COMMENT ON COLUMN worker.password_hash IS '电工APP登录密码bcrypt哈希';
COMMENT ON COLUMN worker.audit_status IS '审核状态:0待审核 1已通过 2已驳回';
COMMENT ON COLUMN worker.audit_remark IS '驳回理由/审核备注';
COMMENT ON COLUMN worker.audit_by IS '审核人(→ sys_user.id)';
COMMENT ON COLUMN worker.audit_at IS '审核时间';
COMMENT ON COLUMN worker.last_login_at IS '最后登录时间';
-- 电工登记台账按审核状态过滤/统计
CREATE INDEX IF NOT EXISTS idx_worker_audit_status ON worker (audit_status);
-- ------------------------------------------------------------
-- 2) 电工资质证照表(电工登记管理的资质附件与到期提醒依据)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS worker_cert (
id BIGSERIAL PRIMARY KEY,
worker_id BIGINT NOT NULL REFERENCES worker(id) ON DELETE CASCADE,
cert_type VARCHAR(64), -- 证书类型(字典 worker_cert_type)
cert_no VARCHAR(64), -- 证书编号
issue_authority VARCHAR(128), -- 发证机关
issue_date DATE, -- 发证日期
expire_date DATE, -- 有效期至(到期提醒依据;NULL=长期有效)
cert_image_url VARCHAR(512), -- 证照图片对象key
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE worker_cert IS '电工资质证照表';
COMMENT ON COLUMN worker_cert.id IS '自增主键';
COMMENT ON COLUMN worker_cert.worker_id IS '共享电工ID(→ worker)';
COMMENT ON COLUMN worker_cert.cert_type IS '证书类型(字典 worker_cert_type)';
COMMENT ON COLUMN worker_cert.cert_no IS '证书编号';
COMMENT ON COLUMN worker_cert.issue_authority IS '发证机关';
COMMENT ON COLUMN worker_cert.issue_date IS '发证日期';
COMMENT ON COLUMN worker_cert.expire_date IS '有效期至(到期提醒依据;NULL=长期有效)';
COMMENT ON COLUMN worker_cert.cert_image_url IS '证照图片对象key';
COMMENT ON COLUMN worker_cert.created_at IS '创建时间';
COMMENT ON COLUMN worker_cert.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_worker_cert_worker ON worker_cert (worker_id);
CREATE INDEX IF NOT EXISTS idx_worker_cert_expire ON worker_cert (expire_date);
CREATE UNIQUE INDEX IF NOT EXISTS uk_worker_cert_type_no ON worker_cert (worker_id, cert_type, cert_no);
-- ------------------------------------------------------------
-- 3) 客户运维服务价目表(运维费定价)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS service_price (
id BIGSERIAL PRIMARY KEY,
price_code VARCHAR(64) NOT NULL UNIQUE,
price_name VARCHAR(64) NOT NULL,
service_type VARCHAR(32),
price_mode SMALLINT NOT NULL DEFAULT 1,
unit_price NUMERIC(10,2) NOT NULL DEFAULT 0,
unit VARCHAR(16),
status SMALLINT NOT NULL DEFAULT 1,
remark VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE service_price IS '客户运维服务价目表';
COMMENT ON COLUMN service_price.id IS '自增主键';
COMMENT ON COLUMN service_price.price_code IS '价目编码(唯一,FW+YYYYMMDD+3位)';
COMMENT ON COLUMN service_price.price_name IS '价目名称';
COMMENT ON COLUMN service_price.service_type IS '服务类型:维修/维保/报警处理/巡检/其他';
COMMENT ON COLUMN service_price.price_mode IS '计价方式:1按次 2按工时 3材料';
COMMENT ON COLUMN service_price.unit_price IS '单价';
COMMENT ON COLUMN service_price.unit IS '计价单位:次/小时/个/米/根';
COMMENT ON COLUMN service_price.status IS '状态:1启用 0停用';
COMMENT ON COLUMN service_price.remark IS '备注';
COMMENT ON COLUMN service_price.created_at IS '创建时间';
COMMENT ON COLUMN service_price.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS uk_service_price_code ON service_price (price_code);
CREATE INDEX IF NOT EXISTS idx_service_price_mode_status ON service_price (price_mode, status);
-- ------------------------------------------------------------
-- 4) dispatch_price 表扩展:电工派单成本价
-- ------------------------------------------------------------
ALTER TABLE dispatch_price ADD COLUMN IF NOT EXISTS price_mode SMALLINT NOT NULL DEFAULT 1; -- 计价方式:1按次 2按工时
ALTER TABLE dispatch_price ADD COLUMN IF NOT EXISTS unit VARCHAR(16) DEFAULT '次'; -- 计价单位
ALTER TABLE dispatch_price ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 1; -- 状态:1启用 0停用
ALTER TABLE dispatch_price ADD COLUMN IF NOT EXISTS remark VARCHAR(255); -- 备注
ALTER TABLE dispatch_price ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
COMMENT ON COLUMN dispatch_price.price_mode IS '计价方式:1按次 2按工时';
COMMENT ON COLUMN dispatch_price.unit IS '计价单位';
COMMENT ON COLUMN dispatch_price.status IS '状态:1启用 0停用';
COMMENT ON COLUMN dispatch_price.remark IS '备注';
COMMENT ON COLUMN dispatch_price.updated_at IS '更新时间';
CREATE INDEX IF NOT EXISTS idx_dispatch_price_status ON dispatch_price (status);
-- ------------------------------------------------------------
-- 5) 站内通知表(当前用途:电工资质证照到期提醒)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGSERIAL PRIMARY KEY,
notice_type VARCHAR(32) NOT NULL,
title VARCHAR(128) NOT NULL,
content VARCHAR(512),
biz_type VARCHAR(32),
biz_id BIGINT,
receiver_id BIGINT NOT NULL,
is_read SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE sys_notice IS '站内通知表';
COMMENT ON COLUMN sys_notice.id IS '自增主键';
COMMENT ON COLUMN sys_notice.notice_type IS '通知类型:cert_expire证照到期';
COMMENT ON COLUMN sys_notice.title IS '通知标题';
COMMENT ON COLUMN sys_notice.content IS '通知内容';
COMMENT ON COLUMN sys_notice.biz_type IS '业务类型:worker_cert电工资质证照';
COMMENT ON COLUMN sys_notice.biz_id IS '业务主键(→ worker_cert.id)';
COMMENT ON COLUMN sys_notice.receiver_id IS '接收人(→ sys_user.id)';
COMMENT ON COLUMN sys_notice.is_read IS '是否已读:0未读 1已读';
COMMENT ON COLUMN sys_notice.created_at IS '创建时间';
CREATE INDEX IF NOT EXISTS idx_sys_notice_receiver ON sys_notice (receiver_id, is_read);
CREATE UNIQUE INDEX IF NOT EXISTS uk_sys_notice_biz ON sys_notice (notice_type, biz_id, receiver_id);
-- ------------------------------------------------------------
-- 6) 菜单与权限
-- 「运营派单」由顶级菜单项改为目录,运维费定价/成本价/电工登记作为其子菜单
-- ------------------------------------------------------------
UPDATE sys_menu SET menu_type = 1 WHERE id = 3;
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
(34, 3, '运维费定价', '/dispatch/price', 'dispatch/price/index', 'MoneyCollectOutlined', 1, 2, 'dispatch:price', 1),
(35, 3, '电工派单成本价', '/dispatch/cost', 'dispatch/cost/index', 'AccountBookOutlined', 2, 2, 'dispatch:cost', 1),
(36, 3, '电工登记管理', '/dispatch/worker', 'dispatch/worker/index', 'ToolOutlined', 3, 2, 'dispatch:worker', 1)
ON CONFLICT (id) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
menu_name = EXCLUDED.menu_name,
menu_path = EXCLUDED.menu_path,
component = EXCLUDED.component,
icon = EXCLUDED.icon,
sort_order = EXCLUDED.sort_order,
menu_type = EXCLUDED.menu_type,
permission = EXCLUDED.permission,
visible = EXCLUDED.visible;
-- 按钮权限(menu_type=3,无路由/组件/图标)
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
(37, 3, '新增价目', NULL, NULL, NULL, 1, 3, 'price:create', 1),
(38, 3, '编辑价目', NULL, NULL, NULL, 2, 3, 'price:update', 1),
(39, 3, '删除价目', NULL, NULL, NULL, 3, 3, 'price:delete', 1),
(40, 3, '新增成本价', NULL, NULL, NULL, 4, 3, 'cost:create', 1),
(41, 3, '编辑成本价', NULL, NULL, NULL, 5, 3, 'cost:update', 1),
(42, 3, '删除成本价', NULL, NULL, NULL, 6, 3, 'cost:delete', 1),
(43, 3, '新增电工', NULL, NULL, NULL, 7, 3, 'worker:create', 1),
(44, 3, '编辑电工', NULL, NULL, NULL, 8, 3, 'worker:update', 1),
(45, 3, '删除电工', NULL, NULL, NULL, 9, 3, 'worker:delete', 1),
(46, 3, '审核电工', NULL, NULL, NULL, 10, 3, 'worker:audit', 1)
ON CONFLICT (id) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
menu_name = EXCLUDED.menu_name,
menu_path = EXCLUDED.menu_path,
component = EXCLUDED.component,
icon = EXCLUDED.icon,
sort_order = EXCLUDED.sort_order,
menu_type = EXCLUDED.menu_type,
permission = EXCLUDED.permission,
visible = EXCLUDED.visible;
-- 角色绑定:ADMIN(role_id=1)拥有 34~46
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
(1, 34), (1, 35), (1, 36), (1, 37), (1, 38), (1, 39), (1, 40), (1, 41), (1, 42), (1, 43), (1, 44), (1, 45), (1, 46)
ON CONFLICT (role_id, menu_id) DO NOTHING;
-- 重置 sys_menu 自增序列,避免后续插入 id 冲突
SELECT setval('sys_menu_id_seq', 46, true);
-- ------------------------------------------------------------
-- 7) 字典(sys_dict 无业务唯一键,先 DELETE 目标 dict_type 再 INSERT,保证幂等)
-- ------------------------------------------------------------
DELETE FROM sys_dict WHERE dict_type IN (
'om_service_type', 'om_price_mode', 'om_price_unit', 'worker_cert_type', 'worker_audit_status'
);
INSERT INTO sys_dict (dict_type, dict_label, dict_value, sort_order) VALUES
('om_service_type', '维修', '维修', 1),
('om_service_type', '维保', '维保', 2),
('om_service_type', '报警处理', '报警处理', 3),
('om_service_type', '巡检', '巡检', 4),
('om_service_type', '其他', '其他', 5),
('om_price_mode', '按次', '1', 1),
('om_price_mode', '按工时', '2', 2),
('om_price_mode', '材料', '3', 3),
('om_price_unit', '次', '次', 1),
('om_price_unit', '小时', '小时', 2),
('om_price_unit', '个', '个', 3),
('om_price_unit', '米', '米', 4),
('om_price_unit', '根', '根', 5),
('worker_cert_type', '低压电工证', '低压电工证', 1),
('worker_cert_type', '高压电工证', '高压电工证', 2),
('worker_cert_type', '特种作业证', '特种作业证', 3),
('worker_cert_type', '登高证', '登高证', 4),
('worker_cert_type', '其他', '其他', 5),
('worker_audit_status', '待审核', '0', 1),
('worker_audit_status', '已通过', '1', 2),
('worker_audit_status', '已驳回', '2', 3);
COMMIT;
@@ -0,0 +1,123 @@
-- 定时任务管理功能扩展
-- 背景:系统周期性后台作业(物化视图刷新、日志清理等)分散在 pg_cron 与应用 @midwayjs/cron 注解中,
-- 运维无法在管理端查看/启停/调频/追溯执行记录。本期新增「自研轻量调度器 + 内置任务注册表」,
-- 新增 sys_job(任务定义与调度配置)、sys_job_log(执行日志)两张表,
-- 并挂载系统管理下的「定时任务」菜单与按钮权限。
-- 幂等:建表/索引用 IF NOT EXISTS;菜单/角色绑定用 ON CONFLICT;任务种子用 ON CONFLICT (job_key) DO NOTHING。
-- 注意:菜单 id 47-50,避开并行规格 add-om-pricing-and-worker-registry 已占用的 34-46。
BEGIN;
-- ------------------------------------------------------------
-- 1) 任务定义与调度配置表
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sys_job (
id BIGSERIAL PRIMARY KEY,
job_key VARCHAR(64) NOT NULL UNIQUE, -- 任务键,与代码注册表一一对应
job_name VARCHAR(64) NOT NULL, -- 任务名称
cron_expression VARCHAR(64) NOT NULL, -- cron 表达式
params JSONB NOT NULL DEFAULT '{}'::jsonb, -- 任务参数(受白名单约束)
retry_times SMALLINT NOT NULL DEFAULT 0, -- 失败后最大重试次数 0-5
retry_interval INT NOT NULL DEFAULT 60, -- 失败后重试间隔(秒 10-3600)
status SMALLINT NOT NULL DEFAULT 1, -- 1启用 0停用
remark VARCHAR(255), -- 备注
last_run_at TIMESTAMPTZ, -- 上次执行时间
last_run_status SMALLINT, -- 上次执行状态:1成功 2失败 3跳过
last_run_cost_ms INT, -- 上次执行耗时(毫秒)
last_error TEXT, -- 上次失败原因(成功时清空)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE sys_job IS '定时任务定义与调度配置表';
COMMENT ON COLUMN sys_job.id IS '自增主键';
COMMENT ON COLUMN sys_job.job_key IS '任务键,与代码注册表一一对应';
COMMENT ON COLUMN sys_job.job_name IS '任务名称';
COMMENT ON COLUMN sys_job.cron_expression IS 'cron 表达式(分 时 日 月 周,支持 6 段带秒)';
COMMENT ON COLUMN sys_job.params IS '任务参数(受白名单字段约束)';
COMMENT ON COLUMN sys_job.retry_times IS '失败后最大重试次数 0-5';
COMMENT ON COLUMN sys_job.retry_interval IS '失败后重试间隔(秒 10-3600)';
COMMENT ON COLUMN sys_job.status IS '状态:1启用 0停用';
COMMENT ON COLUMN sys_job.remark IS '备注';
COMMENT ON COLUMN sys_job.last_run_at IS '上次执行时间';
COMMENT ON COLUMN sys_job.last_run_status IS '上次执行状态:1成功 2失败 3跳过';
COMMENT ON COLUMN sys_job.last_run_cost_ms IS '上次执行耗时(毫秒)';
COMMENT ON COLUMN sys_job.last_error IS '上次失败原因(成功时清空)';
COMMENT ON COLUMN sys_job.created_at IS '创建时间';
COMMENT ON COLUMN sys_job.updated_at IS '更新时间';
CREATE UNIQUE INDEX IF NOT EXISTS uk_sys_job_key ON sys_job (job_key);
CREATE INDEX IF NOT EXISTS idx_sys_job_status ON sys_job (status);
-- ------------------------------------------------------------
-- 2) 任务执行日志表
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS sys_job_log (
id BIGSERIAL PRIMARY KEY,
job_id BIGINT NOT NULL REFERENCES sys_job(id) ON DELETE CASCADE, -- 任务ID(→ sys_job.id)
job_key VARCHAR(64) NOT NULL, -- 任务键(冗余,便于查询)
job_name VARCHAR(64) NOT NULL, -- 任务名称(冗余)
trigger_type SMALLINT NOT NULL DEFAULT 1, -- 触发方式:1定时自动 2手动触发 3失败重试
status SMALLINT NOT NULL, -- 1成功 2失败 3跳过
attempt SMALLINT NOT NULL DEFAULT 1, -- 第几次尝试
started_at TIMESTAMPTZ, -- 开始时间
finished_at TIMESTAMPTZ, -- 结束时间
cost_ms INT, -- 执行耗时(毫秒)
message TEXT, -- 结果摘要
error_stack TEXT, -- 异常堆栈(截断 4000 字符)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE sys_job_log IS '定时任务执行日志表';
COMMENT ON COLUMN sys_job_log.id IS '自增主键';
COMMENT ON COLUMN sys_job_log.job_id IS '任务ID(→ sys_job.id)';
COMMENT ON COLUMN sys_job_log.job_key IS '任务键(冗余,便于查询)';
COMMENT ON COLUMN sys_job_log.job_name IS '任务名称(冗余)';
COMMENT ON COLUMN sys_job_log.trigger_type IS '触发方式:1定时自动 2手动触发 3失败重试';
COMMENT ON COLUMN sys_job_log.status IS '执行状态:1成功 2失败 3跳过';
COMMENT ON COLUMN sys_job_log.attempt IS '第几次尝试';
COMMENT ON COLUMN sys_job_log.started_at IS '开始时间';
COMMENT ON COLUMN sys_job_log.finished_at IS '结束时间';
COMMENT ON COLUMN sys_job_log.cost_ms IS '执行耗时(毫秒)';
COMMENT ON COLUMN sys_job_log.message IS '结果摘要';
COMMENT ON COLUMN sys_job_log.error_stack IS '异常堆栈(截断 4000 字符)';
COMMENT ON COLUMN sys_job_log.created_at IS '创建时间';
CREATE INDEX IF NOT EXISTS idx_sys_job_log_job_created ON sys_job_log (job_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sys_job_log_created ON sys_job_log (created_at);
-- ------------------------------------------------------------
-- 3) 菜单与权限(系统管理 parent_id=6 下新增「定时任务」菜单 + 3 个按钮权限)
-- ------------------------------------------------------------
INSERT INTO sys_menu (id, parent_id, menu_name, menu_path, component, icon, sort_order, menu_type, permission, visible) VALUES
(47, 6, '定时任务', '/system/job', 'system/job/index', 'ClockCircleOutlined', 6, 2, 'system:job', 1),
(48, 47, '编辑定时任务', NULL, NULL, NULL, 1, 3, 'system:job:update', 1),
(49, 47, '立即执行', NULL, NULL, NULL, 2, 3, 'system:job:run', 1),
(50, 47, '清理日志', NULL, NULL, NULL, 3, 3, 'system:job:log:clear', 1)
ON CONFLICT (id) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
menu_name = EXCLUDED.menu_name,
menu_path = EXCLUDED.menu_path,
component = EXCLUDED.component,
icon = EXCLUDED.icon,
sort_order = EXCLUDED.sort_order,
menu_type = EXCLUDED.menu_type,
permission = EXCLUDED.permission,
visible = EXCLUDED.visible;
-- 角色绑定:ADMIN(role_id=1)拥有 47~50
INSERT INTO sys_role_menu (role_id, menu_id) VALUES
(1, 47), (1, 48), (1, 49), (1, 50)
ON CONFLICT (role_id, menu_id) DO NOTHING;
-- 重置 sys_menu 自增序列,避免后续插入 id 冲突
SELECT setval('sys_menu_id_seq', 50, true);
-- ------------------------------------------------------------
-- 4) sys_job 内置任务种子(按 job_key 幂等)
-- ------------------------------------------------------------
INSERT INTO sys_job (job_key, job_name, cron_expression, params, retry_times, retry_interval, status, remark) VALUES
('demo_heartbeat', '示例心跳', '*/5 * * * *', '{"message":"heartbeat"}'::jsonb, 0, 60, 1,
'示例心跳任务:用于验证调度链路,默认每 5 分钟写入一条 info 日志'),
('log_clean', '日志清理', '0 3 * * *', '{"keepDays":90}'::jsonb, 2, 60, 1,
'日志清理任务:每日 03:00 清理早于 keepDays 天的操作日志与任务日志,默认保留 90 天')
ON CONFLICT (job_key) DO NOTHING;
COMMIT;