☰
派出所综合信息管理系统开发实战:数据建模、状态机与权限控制
2026/10/15 4:22:09 网站建设 项目流程

简介:这份资源是面向公安信息化建设人员、派出所基层民警及系统集成从业者的《派出所综合信息管理系统》设计方案文档,围绕“金盾工程”一期一类应用系统的建设要求,梳理派出所综合信息管理系统的整体规划与落地思路,帮助读者理解基层公安应用系统的建设背景、目标与功能框架。压缩包内共1个doc文件,约399KB,内容为完整的公安局派出所综合信息管理系统设计方案,涵盖前言、设计原则、系统总体架构与系统功能设计等章节,并细化到七大子系统、68个模块的划分,涉及信息模型、应用体系架构、系统构造方式及基础业务功能等关键内容。目前已有442人学习下载。读者可从中获取系统建设的总体目标与思路、实战性与安全可靠性等设计原则,以及子系统与模块的拆解方式,适合用于方案撰写参考、项目立项论证或公安信息化教学研究。

1. 派出所综合信息管理系统:从接警到归档,一套系统到底要管多少事

凌晨两点,值班民警接到一起纠纷警情。从接警登记、派警、现场处置、带回调解,到最终形成一份完整的案事件卷宗,中间要经过至少七个环节、四类表单、三个不同角色的审批。如果这些环节还停留在纸质台账加Excel表格的阶段,光是找一份三个月前的询问笔录,就够两个人翻一上午档案柜。派出所综合信息管理系统要解决的,就是把这套流程从“人找表”变成“表找人”。

这个系统本质上是一个面向基层派出所的业务中台,核心覆盖四大模块:警情接处警管理、案事件全流程流转、人口与场所基础信息维护、以及统计报表与考核数据汇总。它服务的对象很明确——基层派出所的接警员、社区民警、办案民警、内勤和所领导。对开发者来说,这类系统的技术难点不在高并发,而在业务状态的精确流转、权限的细粒度控制、以及历史数据的可追溯性。下面从数据建模开始,一步步拆到能跑起来的程度。

2. 先理清业务对象:派出所综合信息管理系统的数据模型怎么搭

2.1 核心实体识别与关系设计

在动手写代码之前,必须把业务对象之间的关系理清楚。派出所综合信息管理系统里最核心的实体有六个:警情(Incident)、案件(Case)、人员(Person)、物品(Item)、场所(Location)、处理记录(HandlingRecord)。它们之间的关系不是简单的树形结构,而是网状交叉。

一起警情可能关联多个人员(报警人、当事人、证人),也可能升级为案件;一个案件可能涉及多个物品(作案工具、赃物);一个人可能同时是某起警情的报警人和另一起案件的嫌疑人。这种多对多关系如果建模时偷懒用JSON字段塞进去,后期查询和统计会非常痛苦。

常见做法是采用“主表+关联表”的模式。以警情和人员的关系为例,不要在主表里存人员ID数组,而是单独建一张关联表:

-- 警情与人员关联表:记录每个警情涉及的人员及其角色 CREATE TABLE incident_person_rel ( id BIGINT PRIMARY KEY AUTO_INCREMENT, incident_id BIGINT NOT NULL COMMENT '警情ID', person_id BIGINT NOT NULL COMMENT '人员ID', role_type TINYINT NOT NULL COMMENT '角色:1报警人 2当事人 3证人 4嫌疑人', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_incident_person_role (incident_id, person_id, role_type), INDEX idx_person_id (person_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='警情人员关联表';

这张表的关键设计点有三个。第一,role_type用TINYINT而不是字符串,因为角色类型是有限枚举,用数字存储查询效率更高,前端展示时再做映射。第二,唯一索引把incident_id、person_id、role_type三个字段组合起来,防止同一个人在同一警情里被重复添加同一角色。第三,单独给person_id建索引,因为“查某人涉及的所有警情”是高频查询场景。

2.2 状态机设计:案事件流转的骨架

案事件的状态流转是整个系统最容易出bug的地方。我见过不少项目用一堆if-else来判断当前状态能不能执行某个操作,结果代码写到后面没人敢改。正确做法是把状态和允许的迁移路径定义成配置,用状态机驱动。

以案件状态为例,典型流转路径是:受理→立案→侦查→破案→结案→归档。每个状态之间的迁移都有条件约束,比如“受理”到“立案”需要满足立案标准,“侦查”到“破案”需要关联至少一个嫌疑人。

# 案件状态机定义:状态迁移规则与条件校验 CASE_STATE_TRANSITIONS = { "accepted": { # 受理 "allowed_next": ["filed", "rejected"], "conditions": { "filed": "check_filing_criteria", # 立案标准校验 "rejected": "check_reject_reason" # 不予立案理由必填 } }, "filed": { # 立案 "allowed_next": ["investigating", "suspended"], "conditions": { "investigating": "check_investigator_assigned", # 必须分配办案人 "suspended": "check_suspend_approval" # 需所领导审批 } }, "investigating": { # 侦查 "allowed_next": ["solved", "suspended"], "conditions": { "solved": "check_suspect_linked", # 至少关联一名嫌疑人 "suspended": "check_suspend_approval" } }, "solved": { # 破案 "allowed_next": ["closed"], "conditions": {"closed": "check_evidence_complete"} # 证据材料齐全 }, "closed": { # 结案 "allowed_next": ["archived"], "conditions": {"archived": "check_archive_materials"} # 归档材料完整 } } def can_transition(current_state, target_state, case_data): """校验状态迁移是否合法""" if current_state not in CASE_STATE_TRANSITIONS: return False, "当前状态不存在" rule = CASE_STATE_TRANSITIONS[current_state] if target_state not in rule["allowed_next"]: return False, f"不允许从{current_state}迁移到{target_state}" # 执行条件校验函数 condition_func_name = rule["conditions"].get(target_state) if condition_func_name: condition_func = globals().get(condition_func_name) if condition_func and not condition_func(case_data): return False, f"不满足迁移条件:{condition_func_name}" return True, "允许迁移"

这段代码的逻辑说明:CASE_STATE_TRANSITIONS字典定义了每个状态的允许迁移目标和对应的校验函数名。can_transition函数先检查目标状态是否在允许列表中,再动态调用校验函数。参数case_data是当前案件的完整数据字典,校验函数从中读取所需字段。

这样设计的好处是,新增状态或调整迁移规则只需要改配置字典,不用动核心逻辑。校验函数单独定义,每个函数只负责一个判断,方便单元测试。实际项目中,我会把状态机配置存到数据库或配置中心,支持运行时热更新,避免每次调整流程都要重新发版。

2.3 权限模型:谁能看什么、改什么

派出所综合信息管理系统的权限比一般业务系统复杂,因为存在“数据隔离”需求。社区民警只能看自己辖区的数据,办案民警只能看自己参与的案件,所领导可以看全所数据。这不是简单的角色权限,而是“角色+数据范围”的双重控制。

常见做法是RBAC加数据权限注解。角色定义操作权限(能不能立案、能不能归档),数据范围定义可见性(本辖区、本办案组、全所)。在查询层统一拦截,而不是在每个接口里手写过滤条件。

// 数据权限注解:标注在Mapper方法上,自动拼接数据范围条件 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface DataScope { // 数据范围类型:1本辖区 2本办案组 3全所 int scopeType() default 1; // 表中用于过滤的字段名 String deptField() default "handle_dept_id"; String userField() default "handle_user_id"; } // AOP切面:在查询前动态拼接SQL条件 @Aspect @Component public class DataScopeAspect { @Before("@annotation(dataScope)") public void doBefore(JoinPoint point, DataScope dataScope) { // 从当前登录用户上下文获取所属部门和用户ID LoginUser user = SecurityUtils.getLoginUser(); StringBuilder sqlCondition = new StringBuilder(); switch (dataScope.scopeType()) { case 1: // 本辖区:按部门ID过滤 sqlCondition.append(" AND ") .append(dataScope.deptField()) .append(" = ").append(user.getDeptId()); break; case 2: // 本办案组:按用户ID过滤 sqlCondition.append(" AND ") .append(dataScope.userField()) .append(" = ").append(user.getUserId()); break; case 3: // 全所:不加过滤条件 break; } // 将拼接好的条件存入ThreadLocal,供MyBatis拦截器读取 DataScopeContext.setCondition(sqlCondition.toString()); } }

逻辑说明:注解@DataScope标注在需要数据权限控制的查询方法上,scopeType指定范围类型。AOP切面在方法执行前,根据当前登录用户的部门ID或用户ID拼接SQL过滤条件,存入ThreadLocal。MyBatis拦截器在生成最终SQL时,从ThreadLocal取出条件追加到WHERE子句。

参数说明:deptField和userField允许不同表使用不同的字段名,默认值覆盖大多数场景。scopeType为3时不加任何条件,适用于所领导查询。这种设计把权限逻辑从业务代码中抽离,新增查询方法时只需加一个注解,不用重复写过滤逻辑。

3. 把接处警流程跑通:从报警登记到警情归档的最小实现

3.1 接警登记接口与必填字段校验

接警登记是整个系统的入口,数据质量直接决定后续环节能不能顺畅流转。我见过因为报警人电话号码少填一位,导致后续回访打不通、案件卡在“无法联系当事人”状态的情况。所以接警登记接口的字段校验必须严格,但也要允许“匿名报警”这类特殊场景。

from datetime import datetime from typing import Optional from pydantic import BaseModel, Field, validator class IncidentCreateRequest(BaseModel): """接警登记请求体""" alarm_type: int = Field(..., description="报警类型:1电话 2上门 3网络 4其他") alarm_content: str = Field(..., min_length=5, max_length=2000, description="报警内容") alarm_time: datetime = Field(..., description="报警时间") alarm_name: Optional[str] = Field(None, max_length=50, description="报警人姓名") alarm_phone: Optional[str] = Field(None, max_length=20, description="报警人电话") address: str = Field(..., max_length=200, description="事发地址") longitude: Optional[float] = Field(None, ge=-180, le=180) latitude: Optional[float] = Field(None, ge=-90, le=90) is_anonymous: bool = Field(False, description="是否匿名报警") @validator('alarm_phone') def validate_phone(cls, v, values): """非匿名报警时电话号码必填且格式校验""" if not values.get('is_anonymous') and not v: raise ValueError('非匿名报警必须填写联系电话') if v and not v.replace('-', '').isdigit(): raise ValueError('电话号码格式不正确') return v @validator('alarm_content') def validate_content(cls, v): """报警内容不能是纯符号或重复字符""" stripped = v.strip() if len(set(stripped)) < 3: raise ValueError('报警内容过于简单,请补充有效信息') return stripped

逻辑说明:使用Pydantic做请求体校验,Field定义字段约束,validator装饰器实现自定义校验逻辑。validate_phone依赖is_anonymous字段的值,所以必须放在is_anonymous之后定义。validate_content检查报警内容的字符多样性,防止接警员随手输入“111”或“aaa”应付。

参数说明:alarm_type用整数枚举,前端下拉框选择。alarm_content限制5到2000字,太短说明信息不足,太长可能是误粘贴。经纬度字段允许为空,因为有些报警人说不清具体位置,后续由出警民警补录。is_anonymous为True时跳过电话校验,但系统会记录接警员工号,确保可追溯。

3.2 警情派发与状态流转的接口实现

接警登记完成后,下一步是派发给处警民警。派发逻辑要考虑几个现实约束:民警当前是否在岗、是否属于事发地辖区、当前手头是否有未完成的警情。我一般会做一个简单的负载判断,优先派给在岗且手头警情少于3条的民警。

def dispatch_incident(incident_id: int, db_session) -> dict: """ 警情派发:根据辖区、在岗状态、当前负载选择处警民警 返回派发结果,包含民警信息和派发时间 """ # 1. 查询警情基本信息 incident = db_session.query(Incident).filter( Incident.id == incident_id, Incident.status == 'pending' # 只有待派发状态才能派发 ).first() if not incident: return {"success": False, "msg": "警情不存在或已派发"} # 2. 确定责任辖区:根据事发地址匹配辖区派出所 jurisdiction = match_jurisdiction(incident.address) if not jurisdiction: return {"success": False, "msg": "无法匹配辖区,请手动指定"} # 3. 筛选候选民警:在岗、属于该辖区、当前警情数<3 candidates = db_session.query(PoliceOfficer).filter( PoliceOfficer.jurisdiction_id == jurisdiction.id, PoliceOfficer.on_duty == True, PoliceOfficer.active_incident_count < 3 ).order_by( PoliceOfficer.active_incident_count.asc(), # 负载少的优先 PoliceOfficer.last_dispatch_time.asc() # 最近派发少的优先 ).all() if not candidates: # 没有符合条件的民警,转为手动派发模式 incident.status = 'manual_dispatch' db_session.commit() return {"success": False, "msg": "无可用民警,请手动派发"} # 4. 选择第一个候选民警并更新状态 assigned_officer = candidates[0] incident.status = 'dispatched' incident.assigned_officer_id = assigned_officer.id incident.dispatch_time = datetime.now() assigned_officer.active_incident_count += 1 assigned_officer.last_dispatch_time = datetime.now() db_session.commit() return { "success": True, "officer_name": assigned_officer.name, "officer_phone": assigned_officer.phone, "dispatch_time": incident.dispatch_time.strftime("%Y-%m-%d %H:%M:%S") }

逻辑说明:函数按四个步骤执行。先校验警情状态,防止重复派发。再根据地址匹配辖区,这一步依赖地址库的维护质量。然后筛选候选民警,排序规则是“负载少优先、最近派发少优先”,避免逮着一个人猛派。最后更新警情和民警状态,提交事务。

参数说明:active_incident_count是民警当前未完成警情数,每次派发加1,警情办结后减1。阈值设为3是经验值,超过3条处警质量会明显下降。last_dispatch_time用于轮询均衡,防止某个民警连续被派发。如果候选为空,警情转为manual_dispatch状态,由值班领导手动指定,避免流程卡死。

3.3 处警反馈与警情归档的闭环

处警民警到达现场后,需要反馈处置结果。反馈内容通常包括:现场情况描述、处置措施、是否转化为案件、涉及人员信息。这个环节的数据要能自动带入后续的案件模块,避免重复录入。

-- 处警反馈表:记录每次处警的详细结果 CREATE TABLE incident_feedback ( id BIGINT PRIMARY KEY AUTO_INCREMENT, incident_id BIGINT NOT NULL COMMENT '关联警情ID', officer_id BIGINT NOT NULL COMMENT '处警民警ID', arrive_time DATETIME NOT NULL COMMENT '到达现场时间', leave_time DATETIME COMMENT '离开现场时间', scene_description TEXT COMMENT '现场情况描述', handling_measure TEXT COMMENT '处置措施', result_type TINYINT NOT NULL COMMENT '处置结果:1现场调解 2带回处理 3转案件 4其他', case_id BIGINT COMMENT '关联案件ID(result_type=3时必填)', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_incident_id (incident_id), INDEX idx_officer_id (officer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处警反馈表'; -- 警情归档:将办结警情移入历史表,主表只保留未办结数据 INSERT INTO incident_archive SELECT *, NOW() as archive_time FROM incident WHERE status = 'closed'; DELETE FROM incident WHERE status = 'closed';

逻辑说明:incident_feedback表记录每次处警的完整信息,result_type决定后续流程走向。当result_type=3时,case_id字段关联自动创建的案件记录。归档操作采用“先插入历史表、再删除主表”的策略,保证数据不丢失的同时,主表保持轻量,查询效率不会随历史数据增长而下降。

参数说明:arrive_time和leave_time用于统计出警时长,是考核指标之一。scene_description和handling_measure用TEXT类型,因为现场情况可能描述得很详细。归档操作建议在业务低峰期执行,比如凌晨,避免锁表影响正常接警。如果数据量特别大,可以按月分表归档。

4. 避坑指南:派出所综合信息管理系统落地时最容易翻车的五个地方

4.1 坑一:状态字段用字符串,后期查询和迁移全是泪

现象:开发初期为了直观,案件状态直接存“已立案”“侦查中”这样的中文字符串。上线三个月后,产品经理说要加一个“已移送”状态,开发发现所有涉及状态判断的代码都要改,而且历史数据里还有“侦查中”和“侦查阶段”两种写法。

原因:状态值没有统一枚举管理,开发人员各自为政,数据库里存的是展示层文案而不是逻辑值。

解决:状态字段一律用数字或英文短码,展示层做映射。所有状态值集中定义在一个枚举类或配置文件中,数据库存逻辑值。迁移时用UPDATE语句批量刷数据,而不是改代码。

# 状态枚举定义:逻辑值与展示文案分离 class CaseStatus: ACCEPTED = 1 # 受理 FILED = 2 # 立案 INVESTIGATING = 3 # 侦查 SOLVED = 4 # 破案 CLOSED = 5 # 结案 ARCHIVED = 6 # 归档 DISPLAY_MAP = { 1: "受理", 2: "立案", 3: "侦查", 4: "破案", 5: "结案", 6: "归档" } @classmethod def get_display(cls, status_code): return cls.DISPLAY_MAP.get(status_code, "未知状态")

4.2 坑二:人员信息重复录入,同一个人的三条记录对不上

现象:报警人张三在警情登记时录了一次,后来作为当事人录了一次,再后来作为嫌疑人又录了一次。三条记录姓名相同但身份证号一个填了、一个没填、一个填错了,统计时无法合并。

原因:人员表没有做唯一性约束,各业务模块各自建人员记录,没有统一的人员主数据管理。

解决:建立统一的人员主表,用身份证号做唯一标识(无身份证的用姓名+出生日期+户籍地组合去重)。各业务模块只存人员ID,不存姓名等冗余信息。录入时先查重,存在则关联,不存在才新建。

-- 人员主表:身份证号唯一,无身份证时用组合键去重 CREATE TABLE person_master ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) COMMENT '身份证号', birth_date DATE, household_address VARCHAR(200) COMMENT '户籍地', phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_id_card (id_card), INDEX idx_name_birth (name, birth_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='人员主表';

4.3 坑三:权限过滤写在业务层,漏一个接口就泄一片数据

现象:社区民警在“我的警情”列表里只能看到自己辖区的数据,但在“综合查询”页面能看到全所数据。排查发现综合查询接口忘了加数据权限过滤。

原因:数据权限逻辑散落在各个Service方法里,靠开发人员自觉添加,没有统一拦截机制。

解决:用AOP或MyBatis拦截器统一处理数据权限,在Mapper层自动拼接过滤条件。开发人员只需在方法上加注解,不依赖人工记忆。同时写单元测试,验证每个查询接口在不同角色下的返回数据范围。

4.4 坑四:附件存储用本地磁盘,服务器一换全丢

现象:询问笔录的扫描件、现场照片存在应用服务器的本地目录,后来服务器迁移,运维只备份了数据库,附件全丢了。

原因:附件存储没有做统一抽象,直接调用了本地文件API,和服务器绑定。

解决:附件统一走对象存储或独立文件服务,数据库只存文件路径或文件ID。上传时计算文件哈希值,相同文件不重复存储。定期做附件完整性校验,对比数据库记录和实际文件。

import hashlib import os def upload_attachment(file_bytes, file_name, upload_dir): """ 附件上传:计算哈希值,按哈希前两位分目录存储 返回文件相对路径,存入数据库 """ file_hash = hashlib.md5(file_bytes).hexdigest() # 按哈希前两位分目录,避免单目录文件过多 sub_dir = file_hash[:2] target_dir = os.path.join(upload_dir, sub_dir) os.makedirs(target_dir, exist_ok=True) # 文件扩展名保留,文件名用哈希值,防止重名覆盖 ext = os.path.splitext(file_name)[1] target_path = os.path.join(target_dir, file_hash + ext) if not os.path.exists(target_path): with open(target_path, 'wb') as f: f.write(file_bytes) # 返回相对路径,数据库存储此值 return os.path.join(sub_dir, file_hash + ext)

4.5 坑五:统计报表实时查询,月底直接把数据库拖垮

现象:每月月底生成考核报表时,系统响应极慢,有时直接超时。排查发现报表SQL在千万级数据上做多表关联和聚合,每次刷新都要跑几十秒。

原因:报表查询直接走业务主库,没有做读写分离,也没有预计算。

解决:报表数据走单独的只读库或数据仓库,通过定时任务预计算好每日汇总数据,报表查询只查汇总表。对于必须实时查询的场景,加缓存并设置合理的过期时间。

-- 每日警情汇总表:定时任务每天凌晨生成前一天数据 CREATE TABLE daily_incident_summary ( id BIGINT PRIMARY KEY AUTO_INCREMENT, summary_date DATE NOT NULL, dept_id BIGINT NOT NULL COMMENT '部门ID', incident_count INT DEFAULT 0 COMMENT '警情总数', case_count INT DEFAULT 0 COMMENT '转案件数', avg_handle_minutes INT DEFAULT 0 COMMENT '平均处置时长(分钟)', UNIQUE KEY uk_date_dept (summary_date, dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日警情汇总表';

5. 让系统经得起用:数据校验、日志追溯与性能兜底的具体做法

5.1 关键字段的二次校验与数据质量巡检

接口层的校验只能挡住格式错误,挡不住逻辑错误。比如身份证号格式正确但校验位不对,或者案件状态是“已结案”但结案时间字段为空。我一般会在数据库层加约束,再配一个定时巡检任务。

-- 案件表增加检查约束:结案状态必须有结案时间 ALTER TABLE cases ADD CONSTRAINT chk_closed_time CHECK ( (status = 5 AND close_time IS NOT NULL) OR (status != 5) ); -- 数据质量巡检:找出身份证号校验位错误的记录 SELECT id, name, id_card FROM person_master WHERE id_card IS NOT NULL AND LENGTH(id_card) = 18 AND SUBSTRING(id_card, 18, 1) != calculate_check_digit(SUBSTRING(id_card, 1, 17));

巡检任务每天跑一次,结果推送给内勤人工核实。不要自动修改数据,因为可能是录入时手误,需要人工确认正确值。

5.2 操作日志:谁在什么时候改了什么

派出所系统的操作日志不是可选项,是必选项。案事件信息涉及当事人权益,任何修改都要留痕。我一般用“变更前后快照”的方式记录,而不是只记一句“修改了案件信息”。

def log_operation(user_id, table_name, record_id, before_data, after_data): """ 记录操作日志:对比变更前后数据,只记录有变化的字段 """ changes = {} for field in after_data: old_val = before_data.get(field) new_val = after_data.get(field) if old_val != new_val: changes[field] = {"before": old_val, "after": new_val} if not changes: return # 没有实际变更,不记录 log_entry = OperationLog( user_id=user_id, table_name=table_name, record_id=record_id, changes=json.dumps(changes, ensure_ascii=False, default=str), created_at=datetime.now() ) db_session.add(log_entry) db_session.commit()

参数说明:before_data和after_data是字典格式的完整记录。changes只存有变化的字段,减少日志体积。default=str处理日期类型无法JSON序列化的问题。日志表按月分表,保留至少三年。

5.3 查询性能兜底:索引、分页与慢查询监控

派出所系统的数据量增长是线性的,但查询需求是随机的。除了常规索引,我会特别关注几个高频查询场景的索引覆盖。比如“按报警人电话查历史警情”,如果只在alarm_phone上建索引,查询时还要回表取其他字段,效率不高。可以建覆盖索引。

-- 覆盖索引:查询报警人历史警情时无需回表 ALTER TABLE incident ADD INDEX idx_phone_status_time (alarm_phone, status, alarm_time); -- 慢查询监控:开启慢查询日志,阈值设为1秒 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;

分页查询用游标分页代替OFFSET分页,避免深分页性能问题。前端传上一页最后一条记录的ID,后端用WHERE id > last_id LIMIT 20的方式查询。

5.4 一个具体技巧:用数据库视图简化复杂查询

派出所系统里有很多跨表统计需求,比如“每个社区民警本月处理的警情数、转案件数、平均处置时长”。如果每次都在代码里拼SQL,维护起来很痛苦。我一般会建几个统计视图,把复杂关联封装起来。

-- 民警月度工作量统计视图 CREATE VIEW v_officer_monthly_stats AS SELECT o.id AS officer_id, o.name AS officer_name, o.jurisdiction_id, DATE_FORMAT(i.dispatch_time, '%Y-%m') AS stat_month, COUNT(DISTINCT i.id) AS incident_count, COUNT(DISTINCT CASE WHEN i.result_type = 3 THEN i.id END) AS case_count, AVG(TIMESTAMPDIFF(MINUTE, i.dispatch_time, f.leave_time)) AS avg_handle_minutes FROM police_officer o LEFT JOIN incident i ON i.assigned_officer_id = o.id LEFT JOIN incident_feedback f ON f.incident_id = i.id WHERE i.dispatch_time IS NOT NULL GROUP BY o.id, o.name, o.jurisdiction_id, DATE_FORMAT(i.dispatch_time, '%Y-%m');

视图的优点是查询逻辑集中管理,多个报表可以复用。缺点是视图不能建索引,数据量大时性能一般。所以视图适合中小数据量场景,数据量上千万后还是要走预计算汇总表。

我自己的习惯是,每接手一个派出所系统项目,第一周不写业务代码,先把状态机、权限模型、日志规范这三样定下来。这三样定不好,后面写多少功能都是返工。状态机决定流程能不能改,权限模型决定数据会不会泄,日志规范决定出了问题能不能查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询