基于Spring Boot与微信小程序的律所合同管理系统设计与实现
简介这份源码面向Java后端开发者、软件工程专业学生及律所信息化建设人员提供一套基于Java开发的律师事务所律师与合同管理系统完整实现解决律所日常事务中档案、合同、案件与用章流程分散、管理效率低的问题。资源包共250个文件约573KB以165个XML配置文件、55个Java源文件为主另含22个日志文件及yml、properties、iml、cmd、mvnw、md等工程与构建文件覆盖配置、业务逻辑与项目组织各层面。系统集成微信小程序与后台管理端功能涵盖用户管理、律师档案、合同管理、案件代理和用章管理后端采用Maven构建与Git版本控制目录结构清晰便于理解分层设计与模块划分。目前已有286人学习下载适合作为课程设计、毕业设计或企业级管理系统的参考案例读者可从中获取完整源码、工程配置与业务模块实现思路快速掌握律所信息化系统的开发要点。1. 律所业务数字化从一份合同流转说起很多律所的信息化改造起点往往不是宏大的“数字化转型”而是一个很具体的麻烦一份委托代理合同从谈案到归档中间要经过律师起草、主任审核、行政盖章、财务登记、档案入库五个环节每个环节都靠微信群里喊一声、纸质表格签个字。合同版本对不上、用章记录查不到、律师档案更新滞后这些事单看都不大堆在一起就是管理上的“黑匣子”——出了问题翻记录谁都说不清。这套基于 Java 开发的律师与合同管理系统要解决的就是这个场景。它把用户权限、律师档案、合同流转、案件代理、用章审批五个模块收进一个后台再通过微信小程序把移动端入口打开让律师在手机上就能提交用章申请、查看案件进度。适合正在做律所信息化管理方向毕设或课设的同学也适合需要一套可运行、可拆解参考方案的开发者。249 个文件、55 个 Java 源文件、165 个 XML 配置规模不算大但模块划分和分层结构是完整的拿来做方案模板刚刚好。2. 需求拆解与技术选型为什么是 Spring Boot 微信小程序2.1 五个业务模块的边界怎么划律所管理系统的需求看起来杂但按“人、事、物”三条线拆就清楚了。人这条线是用户管理和律师档案——用户是系统操作者律师是业务主体两者不能混在一张表里。事这条线是合同管理和案件代理——合同是委托关系的起点案件是合同履行的过程两者有先后依赖但状态机不同。物这条线是用章管理——公章是律所最敏感的实体资源每一次使用都必须有申请、审批、归档的完整链路。模块边界划清楚之后接口就好定了。用户模块只负责认证和权限分配不碰业务数据律师档案模块维护律师的基本信息、执业证号、专业领域供其他模块通过 lawyerId 关联查询合同模块管理合同的生命周期状态从草稿到生效到归档案件模块挂在合同下面一个合同可以对应多个案件用章模块独立走审批流审批通过后回写用章记录。注意模块划分时最容易犯的错是把“律师”和“用户”做成一张表。律师是业务实体用户是登录账号一个律师可能没有系统账号一个行政账号也不对应律师身份。分开建表用中间表关联后面权限控制才不会乱。2.2 后端技术栈的取舍项目用的是 Spring Boot MyBatis 的经典组合Maven 构建Git 版本控制。从文件清单里能看到UserServiceImpl.java、LawyerServiceImpl.java、ContractServiceImpl.java这些服务实现类说明业务逻辑集中在 Service 层Controller 只做参数校验和路由。WebCondig.java通常是 WebConfig 的拼写变体负责跨域配置和拦截器注册MenuServiceImpl.java处理菜单权限树。为什么选 Spring Boot 而不是其他框架对于律所这种中小规模的管理系统Spring Boot 的自动配置和起步依赖能省掉大量 XML 配置。项目里 165 个 XML 文件大部分是 MyBatis 的 Mapper 映射文件而不是 Spring 的 bean 配置这说明配置重心在数据层业务层的注解配置已经足够。// UserServiceImpl.java 核心逻辑示意 Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Autowired private MenuMapper menuMapper; Override public User login(String username, String password) { // 密码用 MD5 加盐存储不存明文 User user userMapper.selectByUsername(username); if (user null) { throw new BusinessException(用户不存在); } String encrypted DigestUtils.md5DigestAsHex( (password user.getSalt()).getBytes() ); if (!encrypted.equals(user.getPassword())) { throw new BusinessException(密码错误); } return user; } Override public ListMenu getMenuTree(Integer userId) { // 根据用户角色查菜单再组装成树形结构 ListMenu menus menuMapper.selectByUserId(userId); return buildTree(menus, 0); } }这段代码有两个设计点值得注意。第一密码存储用 MD5 加盐盐值存在用户表里每个用户不同。虽然现在更推荐 BCrypt但对于课设和毕设级别的项目MD5 加盐已经能说明你理解了“不存明文”这个基本原则。第二菜单权限走的是“用户→角色→菜单”的间接查询而不是在用户表里直接存菜单 ID 列表这样角色调整时不用改用户数据。2.3 微信小程序的集成方式项目集成了微信小程序这意味着后端需要提供一套 RESTful 接口给小程序调用同时后台管理系统走另一套接口。通常的做法是后端用同一套 Service 层Controller 层分两个包一个admin包给后台管理用一个wx包给小程序用。两套接口的鉴权方式不同——后台用 Session 或 JWT小程序用微信的code2Session换 openId再绑定系统用户。小程序端的核心页面一般包括用章申请、合同查看、案件进度、个人档案。用章申请是最适合移动端的场景律师在外面谈完案子直接在小程序提交用章申请审批人收到通知后处理不用回所里填纸质单。3. 总体架构设计分层结构与数据流转3.1 四层架构的职责划分系统采用典型的四层架构Controller 层、Service 层、Mapper 层、数据库层。Controller 层按模块分包每个模块一个 Controller比如UserController.java、ContractController.java、LawyerController.java。Service 层对应模块的 ServiceImpl承载业务逻辑。Mapper 层是 MyBatis 的接口对应的 XML 文件放在resources/mapper目录下。!-- ContractMapper.xml 片段合同分页查询 -- select idselectByPage resultMapBaseResultMap SELECT c.*, l.name AS lawyer_name FROM contract c LEFT JOIN lawyer l ON c.lawyer_id l.id where if testcontractNo ! null and contractNo ! AND c.contract_no LIKE CONCAT(%, #{contractNo}, %) /if if teststatus ! null AND c.status #{status} /if if testlawyerId ! null AND c.lawyer_id #{lawyerId} /if /where ORDER BY c.create_time DESC LIMIT #{offset}, #{pageSize} /select这个查询做了三件事关联律师表取律师姓名、支持按合同编号模糊搜索、支持按状态和律师筛选。分页用 LIMIT 偏移量实现offset和pageSize由前端传入。注意LEFT JOIN而不是INNER JOIN因为合同可能还没分配律师用 LEFT JOIN 才不会丢数据。3.2 数据库表设计的关键字段核心表包括user系统用户、lawyer律师档案、contract合同、case案件、seal_apply用章申请、menu菜单、role角色。每张表都有create_time、update_time、is_deleted三个公共字段删除用逻辑删除而不是物理删除方便追溯。表名关键字段说明contractcontract_no, title, client_name, lawyer_id, status, sign_datestatus 枚举0草稿 1待审 2生效 3归档casecase_no, contract_id, court, judge, hearing_date, resultcontract_id 外键关联合同seal_applyapplicant_id, reason, file_url, status, approver_idstatus 枚举0待审 1通过 2驳回lawyername, license_no, specialty, join_date, phonelicense_no 执业证号唯一合同表的status字段是整个系统的状态机核心。草稿状态可以编辑待审状态只能审核生效状态可以关联案件归档状态只读。每次状态变更都要记录操作日志写进operation_log表这样出了问题能查到是谁在什么时候改的。3.3 前后端数据交互的约定后端统一返回 JSON 格式结构为{ code, msg, data }。code为 200 表示成功500 表示业务异常401 表示未登录。前端根据code判断处理逻辑而不是根据 HTTP 状态码。这样做的好处是业务异常和系统异常能区分开前端弹窗提示更精准。// 小程序端请求封装示意 const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method: method, data: data, header: { token: wx.getStorageSync(token) // 登录后存的 token }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }); reject(res.data.msg); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data.msg); } }, fail: reject }); }); };这段封装把 token 注入、错误提示、登录过期跳转都统一处理了业务页面只需要调request(/contract/list, GET, params)就行。token 存在本地缓存里每次请求自动带上不用每个页面手动传。4. 核心模块实现从律师档案到用章审批4.1 律师档案管理的增删改查与关联校验律师档案模块看起来是简单的 CRUD但有两个地方容易翻车。第一是执业证号的唯一性校验第二是删除律师时关联合同的处理。// LawyerServiceImpl.java 核心方法 Override Transactional public void deleteLawyer(Integer lawyerId) { // 先检查是否有未归档的合同关联 int count contractMapper.countByLawyerId(lawyerId); if (count 0) { throw new BusinessException(该律师名下还有 count 份未归档合同无法删除); } // 逻辑删除不物理删除 lawyerMapper.updateDeleted(lawyerId, 1); }删除前先查关联合同数量有未归档合同就拒绝删除。这是业务规则层面的保护比数据库外键约束更友好因为能给出具体的提示信息。逻辑删除用is_deleted字段标记查询时默认过滤is_deleted 0这样历史数据还在统计报表不会因为删除操作而失真。档案的字段校验用 JSR-303 注解在实体类上做比如NotBlank、Pattern校验手机号格式。Controller 层加Valid注解校验失败会抛MethodArgumentNotValidException全局异常处理器统一捕获后返回友好提示。4.2 合同全生命周期管理合同模块的核心是状态流转。一份合同从创建到归档状态变化路径是草稿→待审→生效→归档。每次状态变更都要校验当前状态是否允许该操作比如只有草稿状态才能编辑只有待审状态才能审核。// ContractServiceImpl.java 状态流转控制 Override public void updateStatus(Integer contractId, Integer targetStatus, Integer operatorId) { Contract contract contractMapper.selectById(contractId); if (contract null) { throw new BusinessException(合同不存在); } // 定义允许的状态迁移 MapInteger, ListInteger allowedTransitions new HashMap(); allowedTransitions.put(0, Arrays.asList(1)); // 草稿 - 待审 allowedTransitions.put(1, Arrays.asList(2, 0)); // 待审 - 生效 或 退回草稿 allowedTransitions.put(2, Arrays.asList(3)); // 生效 - 归档 ListInteger allowed allowedTransitions.get(contract.getStatus()); if (allowed null || !allowed.contains(targetStatus)) { throw new BusinessException(当前状态不允许此操作); } contractMapper.updateStatus(contractId, targetStatus); // 记录操作日志 operationLogMapper.insert(contractId, operatorId, 状态变更为 targetStatus); }用 Map 定义状态迁移规则比一堆 if-else 清晰得多。新增状态时只需要改 Map不用动逻辑代码。操作日志记录合同 ID、操作人、变更内容后面做审计或者排查问题时直接查这张表。合同附件上传用 MultipartFile 接收存到服务器本地目录或者对象存储数据库只存文件路径。文件命名用 UUID 避免重名原始文件名存在数据库里用于展示。4.3 用章审批流的实现用章管理是律所特有的需求公章不能随便盖每次使用都要有申请和审批记录。流程是申请人提交用章申请填写事由、上传文件、选择用章类型→ 审批人审核 → 通过后记录用章时间和盖章人 → 归档。// SealApplyServiceImpl.java 审批逻辑 Override Transactional public void approve(Integer applyId, Integer approverId, boolean pass, String comment) { SealApply apply sealApplyMapper.selectById(applyId); if (apply.getStatus() ! 0) { throw new BusinessException(该申请已处理请勿重复操作); } int newStatus pass ? 1 : 2; sealApplyMapper.updateStatus(applyId, newStatus, approverId, comment); if (pass) { // 审批通过后生成用章记录 SealRecord record new SealRecord(); record.setApplyId(applyId); record.setSealType(apply.getSealType()); record.setUseTime(new Date()); record.setOperatorId(approverId); sealRecordMapper.insert(record); } // 通知申请人小程序订阅消息或站内信 notifyService.send(apply.getApplicantId(), 用章申请 (pass ? 已通过 : 被驳回)); }审批通过后自动生成用章记录这样用章台账是自动维护的不需要行政手动登记。通知用站内信或小程序订阅消息实现申请人能及时知道审批结果。审批意见存在comment字段里驳回时必须填写原因通过时可选填。提示用章审批的并发问题要注意。两个审批人同时打开同一条待审记录一个人先点了通过另一个人再点通过时status ! 0的判断能拦住重复操作。但如果两个人几乎同时执行需要在 SQL 层加WHERE status 0的条件更新用受影响行数判断是否成功。5. 避坑与常见问题排查5.1 跨域配置不生效导致小程序请求失败现象小程序端请求后端接口控制台报request:fail或403但浏览器直接访问接口正常。原因WebCondig.java里的跨域配置只对浏览器生效小程序的请求不走浏览器的同源策略但后端如果配了allowedOrigins为具体域名小程序的请求头里没有 Origin会被拦截。解决跨域配置里allowedOriginPatterns设为*或者针对小程序请求单独放行。同时检查拦截器是否把小程序接口也拦截了登录接口和code2Session接口要加入白名单。5.2 MyBatis 驼峰映射失效导致字段为 null现象数据库字段是contract_no实体类属性是contractNo查询结果里contractNo始终是 null。原因MyBatis 默认不开启驼峰命名映射需要在配置文件里加map-underscore-to-camel-case: true。解决在application.yml的 MyBatis 配置下加这一行。或者在 Mapper XML 里手动写resultMap把数据库字段和 Java 属性一一对应。项目里 165 个 XML 文件大部分都在做这件事说明作者选择了显式映射而不是依赖全局配置这样更可控。5.3 合同状态并发更新导致数据错乱现象两个管理员同时操作同一份合同一个点“审核通过”一个点“退回”最终状态不确定。原因先查后改的逻辑没有加锁两个请求都查到了“待审”状态都认为可以操作。解决用乐观锁或条件更新。在UPDATE语句里加WHERE status #{oldStatus}根据返回的受影响行数判断是否更新成功。如果返回 0说明状态已经被别人改了提示用户刷新后重试。5.4 微信小程序登录态过期未处理现象用户在小程序里操作一段时间后所有请求都返回 401但页面没有跳转到登录页。原因token 过期后请求封装里只做了reject没有统一跳转登录页。解决在请求封装的 401 分支里加wx.navigateTo跳转登录页同时清除本地缓存的 token。注意加一个防抖判断避免多个请求同时返回 401 时重复跳转。5.5 文件上传路径在部署后失效现象本地开发时文件上传正常部署到服务器后上传失败或上传后无法访问。原因上传路径写的是本地绝对路径比如D:/upload/服务器上没有这个目录。或者上传目录没有写权限。解决上传路径配在application.yml里用相对路径或环境变量。部署时确保目录存在且有写权限。文件访问用 Nginx 静态资源映射不要通过后端接口读文件流性能差且容易出问题。6. 验收与答辩怎么证明这套系统真的能跑验收这套系统我一般会按“功能走通、数据可查、异常可控”三条线来查。功能走通是指每个模块的核心操作能完整执行一遍新建律师档案→创建合同→关联案件→提交用章申请→审批通过→查看用章记录。这条链路走完说明模块间的关联关系是对的。数据可查是指每个操作完成后数据库里能查到对应的记录状态字段正确关联字段没有空值。比如合同生效后contract表的status应该是 2operation_log表里应该有一条状态变更记录。异常可控是指故意输入非法数据或跳过步骤操作系统能给出友好提示而不是报 500 错误。比如删除有未归档合同的律师应该提示“该律师名下还有 N 份未归档合同”而不是抛数据库外键异常。答辩时老师最常问的几个问题提前准备好答案为什么用微信小程序而不是直接做 App因为律所用户量不大小程序免安装、开发成本低适合移动办公场景。合同状态为什么用数字而不是枚举字符串数字存储空间小查询效率高前端做映射展示。用章审批的并发怎么处理条件更新加受影响行数判断前面已经写过。验收项检查方法合格标准登录鉴权错误密码登录、token 过期后请求提示友好跳转登录页合同状态流转草稿→待审→生效→归档跳步操作跳步被拦截提示当前状态不允许用章审批重复审批同一申请第二次提示已处理文件上传上传非图片/超大文件有格式和大小限制提示数据关联删除有合同的律师拒绝删除并提示合同数量从那以后我每次做完这类管理系统都会强制走一遍“删一条有依赖的数据”和“跳一步状态操作”这两个操作能暴露大部分关联校验和状态机的问题。希望这套方案和排查清单能帮到你需要完整源码的话可以直接拿去跑一遍对着改比对着看快得多。本文还有配套的精品资源点击获取