资讯详情

资讯详情

Java 项目实战: 交友项目 - 陌生人问题:需求分析

概述陌生人问题是交友项目里的破冰机制每个用户预设一道问题想认识 TA 的陌生人必须回答这道题才能发起搭讪。本文把设置侧的需求、数据模型、接口契约和校验边界一次讲清为后续代码实现铺路。纲要业务背景陌生人社交的破冰难题陌生人问题与好友体系的关系素材说明本文依据的讲义与工程快照版本需求描述设置侧维护自己的问题本文核心消费侧陌生人查看与回答后续实现本文只立契约数据设计tb_question 建表Question 实体与 BasePojo 公共字段接口设计API 速览表 JSON 示例流程时序图保存问题的完整调用链原型线框图通用设置页 设置陌生人问题页异常与边界验证要点常见误解澄清总结业务背景交友类产品有个经典的冷启动矛盾两个互不相识的人第一句话说什么直接发在吗几乎必然被无视直接发好友申请又容易被当成骚扰。交友项目的做法是把第一句话的主动权交给被搭讪的一方——每个用户在自己主页设置一道问题比如你喜欢 java 还是 Php、“你觉得周末应该宅着还是出去走”。陌生人想认识 TA就得先回答这道题。这个设计同时解决了两个问题对发起方问题给了他一个明确的切入点回答本身就是表态对接收方回答的质量天然充当了过滤器——愿意认真回答问题的人动机大概率比群发你好的人真诚。和好友体系的关系要说清楚陌生人问题不依赖好友关系也不由开关控制。它不是开了开关别人才能向我提问那类配置而是我预设一道题任何人都能看到并回答的公开内容。tb_settings表通用设置维护的那张表里只有三个通知开关和陌生人问题没有字段上的关联——问题内容独立存在tb_question表里。两者唯一的交集在展示层通用设置页把设置陌生人问题作为一个菜单项入口。素材说明这一节的素材分布比较散先交代清楚依据避免读者对不上号本章讲义day02-完善个人信息.md只覆盖用户资料管理和 token/异常处理没有通用设置与陌生人问题的小节本章工程快照01-代码/代码02mf-test也还没有 Question、Settings 相关类陌生人问题的完整实现最早出现在附近的人工程快照中Question、QuestionApi、SettingsService.saveQuestion()等数据库建表语句在01-项目介绍与环境搭建/03-资料/mf.sql。涉及到的类与文件分布在各模块里先给一张归属地图以后续为准mf ├── mf-model # 实体与 VO消费方/提供方共享 │ └── src/main/java/com/mf/model/ │ ├── domain/Question.java # 陌生人问题实体继承 BasePojo │ └── domain/BasePojo.java # created/updated 自动填充字段 ├── mf-dubbo │ ├── mf-dubbo-interface │ │ └── .../com/mf/dubbo/api/QuestionApi.java # Dubbo 接口定义 │ └── mf-dubbo-db │ └── .../com/mf/dubbo/ │ ├── api/QuestionApiImpl.java # 提供者实现MyBatis-Plus │ └── mappers/QuestionMapper.java # Mapper操作 tb_question └── mf-app-server # 消费者 └── src/main/java/com/mf/server/ ├── controller/SettingsController.java # POST /users/questions └── service/SettingsService.java # saveQuestion() 业务判断所以本文的数据结构和代码骨架以mf.sql为准业务流程描述以视频讲解为准。这也符合项目项目的整体节奏完善用户信息中先把消费者app-server和提供者dubbo 服务的调用链跑通陌生人问题正是用这条链路练手的功能之一。需求描述整个功能分两侧本次只做设置侧的需求分析消费侧只立契约。设置侧维护自己的问题本文核心用户进入「我的 → 通用设置 → 设置陌生人问题」可以设置或修改自己的问题。规则新用户没有设置过问题页面展示默认文案课程里用的是你喜欢java吗修改后点击确认提交服务端按当前登录用户查询tb_question没有记录就新增有记录就更新——每个用户在表里至多一条记录这是典型的存在即更新upsert语义保存成功后前端无特殊返回接口直接回 200 状态码。消费侧陌生人查看与回答后续实现此处只立契约陌生人查看某用户的资料卡时客户端调GET /mf/strangerQuestions?userIdxxx拿到对方的问题并展示陌生人填写回答后调POST /mf/strangerQuestions服务端把「问题原文 回答内容 回答人信息」组装成一条消息通过环信 IM 推给目标用户走的是私信通道而非落库目标用户在消息列表里看到谁回答了我的问题、回答了什么由此决定是否回复破冰闭环在这里完成。设置侧与消费侧通过tb_question解耦设置侧只负责把user_id → txt这条映射维护好消费侧按 userId 实时读取。数据设计存储选型MySQL一个用户一行课程实现把陌生人问题放MySQLmf-dubbo-db模块 MyBatis-Plus而不是项目里大量使用的 MongoDB。理由可以从数据特征推断每个用户固定一条记录结构完全一致user_id txt没有任何字段弹性需求写入频率极低用户偶尔改一次读取按user_id精确命中且建了索引单行查询没有列表、没有分页、没有复杂查询MongoDB 的文档模型优势在这里用不上。如果它做成问题列表 逐条回答的模式选型可能就倒向 MongoDB 了——但这个产品的形态决定了它就是一行简单关系数据。建表语句来自mf.sql可直接执行CREATETABLEtb_question(idbigint(20)NOTNULLAUTO_INCREMENT,user_idbigint(20)DEFAULTNULLCOMMENT用户id,txtvarchar(200)DEFAULTNULLCOMMENT问题内容,createddatetimeDEFAULTNULL,updateddatetimeDEFAULTNULL,PRIMARYKEY(id)USINGBTREE,KEYuser_id(user_id)USINGBTREE)ENGINEInnoDBAUTO_INCREMENT5DEFAULTCHARSETutf8 ROW_FORMATDYNAMIC;设计上有两个细节值得注意user_id上建了普通索引KEY user_id因为查询入口只有一个findByUserIdtxt上限 200 字符一道问题足够同时约束了前端输入框的最大长度。Question 实体工程里的实体在mf-model模块消费方和提供方共享packagecom.mf.model.domain;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;//陌生人问题DataNoArgsConstructorAllArgsConstructorpublicclassQuestionextendsBasePojo{privateLongid;privateLonguserId;//问题内容privateStringtxt;}created、updated两个字段继承自BasePojo配合 MyBatis-Plus 的自动填充业务代码里不需要手动 set 时间packagecom.mf.model.domain;importcom.baomidou.mybatisplus.annotation.FieldFill;importcom.baomidou.mybatisplus.annotation.TableField;importlombok.Data;importjava.io.Serializable;importjava.util.Date;DatapublicabstractclassBasePojoimplementsSerializable{TableField(fillFieldFill.INSERT)//自动填充privateDatecreated;TableField(fillFieldFill.INSERT_UPDATE)privateDateupdated;}有一点别想当然有人会问匿名提问要不要存提问人 id——这个问题在这套设计里不存在。tb_question存的是被搭讪方自己出的问题没有多条问题、多个提问人的概念搭讪方的身份在消费侧通过环信消息携带不落这张表。接口设计本文涉及的接口只有一个设置侧消费侧两个接口一并列出供对照接口方法路径参数说明设置陌生人问题POST/users/questionsbodycontent存在即更新无则保存成功返回 200查看陌生人问题GET/mf/strangerQuestionsqueryuserId消费侧返回问题文案回复陌生人问题POST/mf/strangerQuestionsbodyuserId、reply消费侧经环信 IM 发私信设置接口的请求与响应示例// 请求 POST /users/questions{content:你觉得周末应该宅着还是出去走}// 响应无 bodyHTTP 状态码 200 即成功两个契约细节当前用户从 token 取请求里只有content不传 userId。身份来自请求头 token经拦截器解析后放进UserHolderThreadLocal服务端用UserHolder.getUserId()拿当前用户——信任客户端传的 userId 是典型的越权漏洞响应用ResponseEntity且无 body这类只关心成败的操作不需要回传数据状态码本身就是结果。流程时序图保存陌生人问题的完整调用链横跨消费者mf-app-server与提供者mf-dubbo-db两个服务MySQL tb_questionQuestionApiImpl(dubbo-db 提供者)QuestionApi(Dubbo RPC)SettingsService(app-server 消费者)SettingsController(app-server 消费者)客户端MySQL tb_questionQuestionApiImpl(dubbo-db 提供者)QuestionApi(Dubbo RPC)SettingsService(app-server 消费者)SettingsController(app-server 消费者)客户端alt[记录不存在首次设置][记录已存在修改]POST /users/questions {content}从 Map 取 contentsaveQuestion(content)UserHolder.getUserId() 取当前用户findByUserId(userId)Dubbo 远程调用select * where user_id ?记录或 nullQuestion 或 nullsave(new Question(userId, txt))Dubbo 远程调用insertcreated/updated 自动填充update(question 置新 txt)Dubbo 远程调用update by idupdated 自动填充返回ResponseEntity 200分层职责正好对应课程强调的消费者/提供者拆分controller 取参、service 做业务判断有则更新无则保存、与数据库相关的全部动作下沉到 API 层经 Dubbo 完成。app-server 自身不配 MySQL 数据源这就是拆分的意义。原型线框图原型文件我的.html / 我的.txt里有两个相关页面一屏一图。第一屏通用设置页设置陌生人问题是菜单项入口。-------------------------------------- | - 返回 通用设置 | -------------------------------------- | 设置陌生人问题 | -------------------------------------- | 通知设置 | -------------------------------------- | 黑名单 | -------------------------------------- | 安全设置 | -------------------------------------- | | | [ 退 出 登 录 ] | --------------------------------------要点「设置陌生人问题」在列表第一位点击进入第二屏「通知设置」「黑名单」分别对应通用设置的另两个子功能退出登录位于页面底部独立危险操作区。第二屏设置陌生人问题页。-------------------------------------- | - 返回 设置陌生人问题 | -------------------------------------- | | | -------------------------------- | | | 你喜欢java还是Php | | | | | | | -------------------------------- | | | | [ 确 认 ] | | | --------------------------------------要点输入框预填当前已设置的问题新用户无记录时展示默认文案服务端兜底你喜欢java吗见SettingsService.settings()文本长度受txt varchar(200)约束输入框应设 maxlength点击「确认」调POST /users/questions成功后返回上一页。异常与边界场景处理方式说明用户未设置过问题查询返回 null走保存分支正常路径不是异常已设置过再次提交走更新分支仍只有一个记录禁止插入第二条否则 findByUserId 的 selectOne 会查多条报错content 为空 / 空白应校验并拒绝课程实现未做校验属可补强的点见下文content 超过 200 字前端 maxlength 截断服务端最好二次校验超长会被 MySQL 严格模式下拒绝写入token 无效 / 未登录拦截器统一拦截拿不到 userId不会走到业务逻辑消费侧查无此人的问题服务端兜底默认文案你喜欢java编程吗见mfService.strangerQuestions()频繁重复提交可按 userId 加简单频控本功能写多读少、影响面小一句话点到即可不必过度设计content 为空这类校验课程代码确实省了配合项目里已有的统一异常处理BusinessExceptionExceptionAdvice补一个参数校验是几行代码的事后续代码实现会提到。验证要点首次设置新用户提交问题确认tb_question新增一行user_id、txt、created正确重复修改同一用户再次提交不同内容确认仍是那一行txt与updated更新、created不变边界用两个不同账号分别设置确认互不影响每个 user_id 一行展示回显设置后再打开通用设置页GET /users/settings返回的strangerQuestion是新内容消费侧联动对照 07 章另一账号查看该用户资料卡能看到刚设置的问题文案。常见误解澄清误解事实陌生人问题存在 MongoDB 里实际在 MySQLtb_question表走mf-dubbo-db MyBatis-Plus项目里用 MongoDB 的是评论、动态这类海量文档数据tb_settings 里有 stranger_questions 开关控制能否被提问没有。tb_settings只有三个通知开关陌生人问题没有开关语义是公开的破冰内容一个用户可以设置多个问题不能。一个 user_id 一行记录逻辑是存在即更新提交问题接口需要传 userId不需要也不应该。当前用户从 token 解析客户端传 userId 反而是越权隐患回答问题的记录也存在 tb_question不存在。回答经环信 IM 以私信形式送达不落库tb_question 只存用户自己出的问题该功能会返回丰富的响应体不会成功即 200 空响应成败靠状态码表达API 速览接口方法路径关键参数归属设置陌生人问题POST/users/questionscontentbody本wen查询通用设置GET/users/settings无token 识别用户通用设置查看陌生人问题GET/mf/strangerQuestionsuserIdquery之前回复陌生人问题POST/mf/strangerQuestionsuserId、replybody之前官方文档MyBatis-Plus 自动填充Spring ResponseEntityApache Dubbo 服务提供者总结陌生人问题是破冰机制被搭讪方出题、搭讪方答题用回答质量过滤搭讪动机设置侧的业务本质是一个 upsert按 token 解析出的 userId 查tb_question无则 insert、有则 update一个用户一行存储选 MySQL 而非 MongoDB依据是数据形态——单行、结构固定、按 user_id 点查调用链完整走了一遍消费者/提供者拆分controller 取参、service 业务判断、API 层经 Dubbo 落库消费侧查看问题、回答问题在后续实现回答经环信 IM 私信送达不落库。后续是《陌生人问题代码实现》把这条链路的 Controller、Service、QuestionApi 逐层写出来。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →