ARTICLE · 1358197
JAVA在线律师咨询在线咨询服务系统源码支持小程序+公众号+H5
|
法律咨询最怕「接不上」:用户描述完问题找不到对应领域的律师,律师看不到订单全貌,平台不知道哪一单超时未接——这些环节散落在聊天记录和表格里,出了问题很难追溯。
这套系统做的事,是把「找律师 — 下订单 — 接单 — 咨询 — 结算」串成一条后台可查的链路,用户在小程序、公众号、H5 提问,律师在工作台接单,管理员在后台审认证,各自看到各自那一面。
下面按 9 个功能点拆开写:每一节讲清接什么输入、内部状态怎么变、最后输出什么,再给三端分工和 7 段能直接对照的代码。
OVERVIEW / 功能总览
九个模块,跑通一次在线咨询
不是功能清单,是一条链路:律师建档并通过认证,用户按分类选人下单,律师接单咨询,平台看会员权益与状态流转。
|
01律师管理:档案与擅长领域
输入管理员录入律师姓名、执业证号、从业年限、擅长领域与简介,关联平台账号;这些数据落成 t_lawyer 里的一条主档,不是一张名片。
状态律师在「在线 / 离线」和「待审核 / 已通过 / 已驳回」两条状态线上各自流转;领域改动后,首页列表与筛选条件同步更新。
输出一个可被精准分类检索、可被订单引用的律师档案,首页律师卡片直接取用它。

【在线律师咨询】实拍 01|首页分类与律师卡片、律师详情、我的页面与订单入口;截图数据为演示数据
02咨询管理:订单全链路留痕
输入用户选择领域、咨询方式与问题描述,提交后生成带请求号的咨询订单,包含咨询类型、内容与金额。
状态订单在「待接单 → 咨询中 → 已完成 / 已取消」之间流转,每次变更带上 version,状态与时间戳都记在库里。
输出一条可在用户端、律师工作台、管理后台三处查询的订单记录,配合索引按用户或律师筛选。

【在线律师咨询】实拍 02|咨询下单与支付方式、零钱明细、律师工作台接单状态;截图数据为演示数据
03个人中心:一个入口看全量
输入用户登录后进入个人中心,输入是当前账号的 user_id,系统按它聚合咨询、零钱、权益与资料。
状态页面展示累计咨询数、评价数、可提现收益与认证状态;切换用户端 / 律师端时,入口跟着角色变化。
输出一个把「我的订单、我的零钱、我的权益、认证资料」收在一起的页面,减少来回找入口。
04会员权益:次数与有效期
输入用户购买或领取月卡、季卡、年卡,系统写入等级、免费咨询次数与到期时间。
状态下单时按条件扣减免费次数,次数不足或已过期就拒绝并回滚;会员折扣、专属券与生效状态由同一份权益数据驱动。
输出会员页能看到生效中的套餐、剩余天数与已享权益,下单时自动按权益计价。
05精准分类:按领域找对人
输入运营维护两级分类(婚姻家事、合同债务、劳动人事等),律师建档时绑定其中一个领域。
状态分类树只返回启用项,首页图标、筛选下拉与律师列表共用同一套分类 id,不各写一份。
输出用户点一个领域就能筛出对应律师,避免「什么都会」式的模糊匹配。
06图文咨询:描述与图片留痕
输入用户填写问题描述、上传历史材料图片,选择图文咨询并提交,内容随订单一起落库。
状态律师在工作台看到完整描述与图片,回复按时间正序排列;订单完成前双方可见,完成后归档。
输出一条有文字、有附件、有回复链的咨询记录,后续有争议可回溯到原始描述。
07电话咨询:预约与记录归档
输入用户选择电话咨询、填可接通时段与问题摘要,系统生成同样结构的订单,只是 consult_type 不同。
状态接单后进入咨询中,通话结果由律师回填;未接单超时按规则退款,状态回到已取消。
输出和图文咨询共用一张订单表的电话记录,后台能按咨询类型分别统计。
08接单状态:防并发抢单
输入律师点「接单」,输入是订单 id、律师 id 与当前 version。
状态先用条件更新占用「在线且空闲」的律师,再按 version 改订单状态,任一步影响行数为 0 就抛错回滚;两人同时点只有一人成功。
输出一条确定的接单结果与实时回显的在线状态,不会出现一单被两个律师接走。
09认证管理:资质审核闭环
输入律师提交执业证号与资质材料,管理员在 Vue 后台逐条查看。
状态认证在「待审核 → 已通过 / 已驳回」之间流转,只有管理员角色能改;未通过的律师不进入可接单列表。
输出一份带审核记录的认证结果,前台「已认证」标识与后台列表取同一个字段。

【在线律师咨询】实拍 03|咨询会话消息、律师主页资料、会员权益与续费;截图数据为演示数据
ARCHITECTURE / 三端分工
三端分工:哪些必须在服务端
|
一句话原则:凡影响订单状态、权益次数、认证结果和「谁能看到谁的资料」的判断,都必须在服务端算,前端只负责把结果展示好。
CODE / 数据库与核心代码
数据库与核心代码
下面 7 段是能直接对照的代码:从表结构到接单幂等,覆盖这条链路上最容易出问题的几个点。
以下均为示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
01表结构:四张表装下律师到订单
分类、律师、会员权益、咨询订单各司其职,订单表用 uk_request 唯一键挡住重复提交。
-- 在线律师咨询系统:核心表(MySQL 8.0)CREATE TABLE t_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0, name VARCHAR(32) NOT NULL, sort INT DEFAULT 0, KEY idx_parent (parent_id));CREATE TABLE t_lawyer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, real_name VARCHAR(32) NOT NULL, cert_no VARCHAR(64) DEFAULT '', category_id BIGINT NOT NULL, service_years INT DEFAULT 0, online TINYINT DEFAULT 0, accept_status TINYINT DEFAULT 0, cert_status TINYINT DEFAULT 0, version INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user (user_id), KEY idx_cat (category_id, online));CREATE TABLE t_member_benefit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, level TINYINT DEFAULT 0, free_times INT DEFAULT 0, expire_at DATETIME, version INT DEFAULT 0, UNIQUE KEY uk_user (user_id));CREATE TABLE t_consult_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_no VARCHAR(40) NOT NULL, user_id BIGINT NOT NULL, lawyer_id BIGINT DEFAULT 0, category_id BIGINT NOT NULL, consult_type TINYINT DEFAULT 1, content VARCHAR(1000) DEFAULT '', amount INT DEFAULT 0, status TINYINT DEFAULT 0, version INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_request (request_no), KEY idx_user (user_id, status), KEY idx_lawyer (lawyer_id, status)); |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
02下单:落订单与扣次数同事务
订单落库与权益次数扣减必须同事务,否则会出现单建好了但次数没扣。
// 下单:落订单 + 扣会员次数,同一事务@Transactional(rollbackFor = Exception.class)public OrderVO create(OrderDTO o, Long uid) { // 条件扣减,次数不足时影响行数为 0 int n = benefitMapper.deduct(uid); if (n == 0) { throw new BizException("权益次数不足"); } ConsultOrder d = new ConsultOrder(); d.setRequestNo(o.getRequestNo()); d.setUserId(uid); d.setLawyerId(0L); d.setCategoryId(o.getCategoryId()); d.setConsultType(o.getConsultType()); d.setContent(o.getContent()); d.setAmount(o.getAmount()); d.setStatus(0); orderMapper.insert(d); return OrderVO.of(d);}// 权益次数只按条件扣,不做先查后改public interface BenefitMapper { @Update("UPDATE t_member_benefit " + "SET free_times = free_times - 1 " + "WHERE user_id = #{uid} " + "AND free_times >= 1 " + "AND expire_at > NOW()") int deduct(@Param("uid") Long uid);} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
03接单:条件更新防并发抢单
两个律师同时点接单,靠 version 与影响行数判定,只有一个能成功。
// 接单:条件更新 + version,避免并发抢单public void accept(Long orderId, Long lawyerId, Long uid, Integer version) { // 先占律师:本人、在线、当前未接单 int l = lawyerMapper.occupy(lawyerId, uid); if (l == 0) { throw new BizException("律师状态已变化"); } // 再改订单,只认影响行数 int n = orderMapper.accept( orderId, lawyerId, version); if (n == 0) { throw new BizException("订单已被接走"); }}public interface OrderMapper { @Update("UPDATE t_consult_order " + "SET status=1, lawyer_id=#{lid}, " + "version=version+1 " + "WHERE id=#{id} AND status=0 " + "AND version=#{v}") int accept(@Param("id") Long id, @Param("lid") Long lid, @Param("v") Integer v);}// 律师接单状态:在线且空闲才占用@Update("UPDATE t_lawyer " + "SET accept_status=1, " + "version=version+1 " + "WHERE id=#{lid} AND user_id=#{uid} " + "AND online=1 AND accept_status=0 " + "AND version=#{v}")int occupy(@Param("lid") Long lid, @Param("uid") Long uid, @Param("v") Integer v); |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
04幂等:请求号 + Redis + 唯一键
请求号查重、Redis 抢占、唯一键兜底三层,重复提交只产生一单。
// 下单幂等:请求号 + Redis + 唯一键public OrderVO once(OrderDTO o, Long uid) { String rq = o.getRequestNo(); // 1. 请求号查重,命中直接返回 ConsultOrder old = orderMapper.byRequestNo(rq); if (old != null) { return OrderVO.of(old); } // 2. Redis 抢占,防并发重复提交 boolean ok = redis.setIfAbsent( "order:" + rq, "1", 30); if (!ok) { throw new BizException("请求处理中"); } try { return create(o, uid); } catch (DuplicateKeyException e) { // 3. 只放过 uk_request 冲突 String msg = "" + e.getMessage(); if (!msg.contains("uk_request")) { throw e; } ConsultOrder d = orderMapper.byRequestNo(rq); return OrderVO.of(d); }} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
05权限:律师只看自己的咨询
认证审核只有管理员可做,律师查订单时显式校验归属,不靠前端传参。
// 权限:律师只看自己订单,认证需管理员@PreAuthorize("hasRole('LAWYER')")public OrderVO detail(Long orderId, Long uid) { // 显式校验归属,不靠前端传参 ConsultOrder d = orderMapper.byId(orderId); if (d == null) { throw new BizException("订单不存在"); } Lawyer lw = lawyerMapper.byUserId(uid); if (lw == null || !lw.getId().equals(d.getLawyerId())) { throw new BizException("无权查看该咨询"); } return OrderVO.of(d);}// 认证管理:仅管理员可审核@PreAuthorize("hasRole('ADMIN')")public void certify(Long lawyerId, int status) { if (status != 1 && status != 2) { throw new BizException("状态不合法"); } lawyerMapper.updateCert(lawyerId, status);}// 精准分类:只返回启用的一级/二级public List<CategoryVO> tree() { return categoryMapper.tree(0L);} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
06多端请求:统一带 token 与请求号
小程序、公众号、H5 共用一套请求封装,401 统一跳登录。
// 多端统一请求:小程序 + 公众号 + H5const BASE = 'https://api.example.com';function platform() { // #ifdef MP-WEIXIN return 'mp'; // #endif // #ifdef H5 return 'h5'; // #endif return 'wx';}function genRequestNo() { return Date.now() + '-' + Math.random().toString(36).slice(2, 10);}export function request(path, data = {}) { return new Promise((resolve, reject) => { const rq = genRequestNo(); uni.request({ url: BASE + path, method: 'POST', data: { ...data, requestNo: rq }, header: { 'X-Token': uni.getStorageSync('token'), 'X-Request-No': rq, 'X-Platform': platform() }, success(res) { if (res.statusCode === 401) { uni.navigateTo({ url: '/pages/login' }); reject(new Error('未登录')); return; } if (res.data.code === 0) { resolve(res.data.data); } else { reject(new Error(res.data.msg)); } }, fail: reject }); });}// 图文咨询 / 电话咨询共用一个下单口export function createConsult(o) { return request('/consult/create', o);} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
07后台表格:认证审核与状态标签
后台要的是分页列表、状态标签和通过 / 驳回两个操作,不堆花哨交互。
<template> <div class="cert-list"> <el-table :data="list" v-loading="loading"> <el-table-column prop="realName" label="律师" /> <el-table-column prop="certNo" label="执业证号" /> <el-table-column label="认证状态"> <template #default="{ row }"> <el-tag :type="tagType(row.certStatus)"> {{ statusText(row.certStatus) }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="180"> <template #default="{ row }"> <el-button size="mini" @click="pass(row)">通过</el-button> <el-button size="mini" type="danger" @click="reject(row)">驳回</el-button> </template> </el-table-column> </el-table> <el-pagination :current-page="page" :page-size="20" :total="total" layout="prev, pager, next" @current-change="load" /> </div></template><script>export default { data() { return { list: [], page: 1, total: 0, loading: false }; }, created() { this.load(1); }, methods: { tagType(s) { return s === 1 ? 'success' : s === 2 ? 'danger' : 'warning'; }, statusText(s) { return s === 1 ? '已通过' : s === 2 ? '已驳回' : '待审核'; }, async load(p) { this.page = p; this.loading = true; const res = await api.certList({ page: p }); this.list = res.records; this.total = res.total; this.loading = false; }, async audit(row, status) { await api.certify(row.id, status); this.load(this.page); }, pass(row) { this.audit(row, 1); }, reject(row) { this.audit(row, 2); } }};</script> |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流等。
COMPLIANCE / 合规与上线
合规与上线检查
执业资质:律师执业证号与认证材料需可核验,未通过认证的账号不得展示「已认证」标识,也不得以律师身份接单。
个人信息:用户描述、上传材料与联系方式按最小必要收集,存储加密,查看与导出留痕。
咨询内容:图文描述与通话记录属敏感内容,需权限隔离、留存期限与删除机制,日志不打印明文。
服务边界:平台提供信息撮合与记录,不替代法律意见、不对结果作承诺,页面需有明确提示。
上线前建议逐项打勾
|
FIT / 适合谁
适合谁
适合想做律师在线咨询平台、律所线上获客或法律服务撮合的团队:需要把律师、订单、会员与认证收进一套后台。
不太适合只需要一个律所介绍页、不做下单与接单的场景,那是官网,不是这套系统。
二次开发表结构、状态机与三端分工已定好,换品牌、换领域分类就能用,适合行业二开的开发者与小团队。
FAQ / 常见问题
五个被问得最多的问题
Q:为什么接单要用 version 而不是先查再改?A:先查再改在并发下两个律师都会读到「待接单」,会一单两接;条件更新加 version 只认影响行数,谁先改成功谁接走。
Q:图文咨询和电话咨询要分两张表吗?A:不用。两者结构一样,用 consult_type 区分即可,后台按类型分别统计,维护成本更低。
Q:三端(小程序 / 公众号 / H5)代码能共用吗?A:能。uni-app 一套代码编译到三端,请求层统一封装,只有 platform 标记与登录方式不同。
Q:会员免费次数怎么防止被刷?A:用条件更新 free_times >= 1 扣减,配合请求号唯一键与事务,重复提交不会多扣也不会多建单。
Q:能不能只买小程序端,不要管理后台?A:不能。律师建档、认证审核、分类维护都要后台,服务端接口也需要后台调用;小程序只是展示与下单端。
LAST / 写在最后
这套系统的价值不在功能多少,而在每一环都有记录可查:谁建的律师档案、谁审的认证、谁接的单、权益次数扣在哪一单上。
代码是示例实现,表结构和状态机可以按实际业务调整;发布前建议按上面的打勾清单过一遍权限与合规。
如果你正在做律师在线咨询平台,或想基于 SpringBoot + MyBatisPlus + uni-app 做一套可二开的系统,欢迎找我领取演示资料和部署说明。
文中截图均为演示数据,界面与文案随版本可能调整;具体功能以实际交付版本为准。















































































