好友申请已通过,为什么不等于仍是好友?事务与重复提交的边界

简介: 好友申请“已通过”仅表示历史操作完成,不等于当前仍是好友;关系删除后,旧申请重试不会恢复关系。本文通过双表模型与事务设计,厘清申请状态与好友关系的独立性,避免重复建联。(239字)

好友申请列表里显示“已通过”,就能据此认定两个人现在仍是好友吗?如果后来删除了好友,网络重试又把旧的“通过申请”请求发来一次,系统会不会悄悄把关系恢复?

这两个问题来自同一个容易被忽略的区别:申请状态记录一次操作的处理结果,好友关系描述当前两个人之间的连接。 把两者合成一个 isFriend 字段,既难解释历史记录,也容易让重复请求产生新的副作用。

米米商聊的产品方手机端资料确认,添加好友需要对方通过申请,转发好友名片后仍需发起申请,并有删除联系人等操作。本文以这些真实功能为背景,分析同类系统如何划分申请与关系。资料没有说明其数据库、重复请求策略或桌面端完整审批流程,以下状态、表结构和代码均为独立教学模型,不代表产品内部实现。文章与配图使用 AI 辅助,示例另做独立验证。

1. “处理过申请”与“仍有关系”是两次查询

可以把问题拆成两个对象:

对象 回答的问题 教学示例
一次好友申请 谁向谁申请,这一次是否已经处理 r1 从 pending 变为 accepted
当前好友关系 此刻两个人之间是否存在关系 账号 1 与 2 的关系记录存在或不存在

因此,申请已经通过而当前关系不存在,并不必然是数据损坏:关系可能在申请完成之后被删除。反过来,当前已经是好友,也不能自动证明某一条新申请已经处理。

这不是为产品补充未确认的规则。它只是一个通用建模选择:保存历史处理结果,同时单独管理当前关系。删除关系要不要保留申请记录、保存多久、怎样向用户展示,应由实际产品规则与数据保留要求决定。

2. 重复提交不能把“曾经成功”变成“再次建立”

设想一组纯教学操作,账号与申请标识均为虚构数据:

  1. 账号 1 发起申请 r1,账号 2 通过,建立关系。
  2. 之后这段关系被删除,但 r1 仍记录为已通过。
  3. 旧的通过操作因重试再次到达。

申请历史与当前好友关系的双线时间图

图:独立教学模型。同一申请的历史结果保持已通过,当前关系可以被后续删除;旧申请重试不重新建关系。不是 App 界面或产品实际数据库。

如果处理函数每次看到 accepted 都补插一条关系,就会把第三步解释成一次新的同意。“保证重试不重复建行”还不够;关系删除以后,旧请求也不应重新创建已经结束的关系。

本文选择的策略是:同一申请已经处理成功,再次执行只返回“已经处理”,不写好友关系;重新建立关系需要另一条申请获得新的明确处理。这里的“新的申请”不是简单换一个请求标识绕过限制,实际服务仍需要检查当前申请与关系规则。

幂等的对象在这里是“一次申请的通过操作”,不是“无论何时调用,都保证这对账号存在好友关系”。历史成功结果可以复用,已经发生过的副作用不能在后续状态变化后重新执行。

3. 用两个表约束一次申请与一对关系

下面使用 SQLite 演示。账号标识假定为稳定的正整数;关系以较小账号、较大账号组成规范化二元组,避免 (1, 2) 与 (2, 1) 变成两条记录。这不是按昵称排序,也不要求真实产品采用数字账号。

CREATE TABLE friend_request (
  request_id TEXT PRIMARY KEY NOT NULL CHECK(length(request_id) > 0),
  sender_id INTEGER NOT NULL CHECK(sender_id > 0),
  recipient_id INTEGER NOT NULL CHECK(recipient_id > 0),
  status TEXT NOT NULL DEFAULT 'pending'
    CHECK(status IN ('pending', 'accepted', 'rejected')),
  CHECK(sender_id <> recipient_id)
) STRICT;

CREATE TABLE friendship (
  user_low INTEGER NOT NULL CHECK(user_low > 0),
  user_high INTEGER NOT NULL,
  PRIMARY KEY(user_low, user_high),
  CHECK(user_low < user_high)
) STRICT;

STRICT 是本例对字段类型的选择,需要 SQLite 3.37.0 或以上;它仍允许可无损转换的类型值,并不替代业务输入校验。正式系统还需要账号存在性等约束,本例省略了账号表,没有把正整数检查写成账号真实性证明。SQLite:STRICT Tables

申请表保留方向,关系表只保存本例的对称连接。这里不实现黑名单、单向联系人或其他访问规则;这些规则如果存在,应独立定义,不能仅从这张对称关系表推断。

4. 在一个事务中“通过申请并建立关系”

只先更新申请,会出现“已通过但关系没有写入”;只先写关系,会出现“关系存在但申请还显示待处理”。两个写入需要共同提交,失败则共同回滚。

下面的 Python 函数使用专用连接,调用时连接不应已经处于其他事务中。actor_id 应从服务端认证会话取得,不能直接信任客户端提交的“当前用户”。

def accept_request(db, request_id, actor_id):
    if type(actor_id) is not int or actor_id <= 0:
        raise PermissionError("invalid-actor")
    db.execute("BEGIN IMMEDIATE")
    try:
        row = db.execute(
            "SELECT sender_id, recipient_id, status FROM friend_request "
            "WHERE request_id = ?", (request_id,)
        ).fetchone()
        if row is None:
            raise ValueError("request-unavailable")
        sender, recipient, status = row
        if actor_id != recipient:
            raise PermissionError("not-recipient")
        if status == "accepted":
            db.execute("COMMIT")
            return "already-processed"
        if status != "pending":
            raise ValueError("not-pending")

        low, high = sorted((sender, recipient))
        db.execute(
            "INSERT INTO friendship VALUES (?, ?) "
            "ON CONFLICT(user_low, user_high) DO NOTHING", (low, high)
        )
        changed = db.execute(
            "UPDATE friend_request SET status = 'accepted' "
            "WHERE request_id = ? AND status = 'pending'", (request_id,)
        ).rowcount
        if changed != 1:
            raise RuntimeError("state-conflict")
        db.execute("COMMIT")
        return "accepted"
    except Exception:
        if db.in_transaction:
            db.execute("ROLLBACK")
        raise

几个边界比语句顺序更重要:

  • 先核对处理者。 发起人和无关账号都不能代收件人同意申请。示例异常供内部测试区分原因,真实接口怎样返回错误还需考虑信息暴露,不能原样当作公开接口规范。
  • 已处理分支不补插关系。 这正是删除之后重试不会恢复关系的原因。不能用“发现关系缺失”替换这一分支。
  • 只处理明确的唯一键冲突。 ON CONFLICT(user_low, user_high) DO NOTHING 允许当前关系已经存在,随后仍明确处理这条待处理申请;其他约束错误会进入失败处理。SQLite 官方文档说明,UPSERT 针对唯一性约束,不会吞掉 CHECK、NOT NULL 等失败。SQLite:UPSERT
  • 确认提交后才返回成功。 BEGIN IMMEDIATE 尽早申请写事务,但不保证一定拿到它。SQLite 同时只允许一个写事务,另一个写入者可能导致忙错误;本例将失败交给调用层处理,不在函数里无限重试。SQLite:Transaction

连接创建时使用 isolation_level=None,由代码显式执行 BEGIN、COMMIT 和 ROLLBACK;不要再叠加另一层隐式事务管理。如果应用已有事务边界,应调整函数职责,而不是嵌套 BEGIN。Python 官方文档解释了该模式与显式 SQL 事务的关系。Python 3.12:Transaction control

5. 直接验证“通过、删除、旧请求重试”

把第一个 SQL 代码块原样保存为 UTF-8 的 schema.sql,将下面代码接在前面的 Python 函数后运行。它只创建内存教学数据库,不读取真实联系人。

import sqlite3
from pathlib import Path

db = sqlite3.connect(":memory:", isolation_level=None)
db.executescript(Path("schema.sql").read_text(encoding="utf-8"))
db.execute("INSERT INTO friend_request VALUES ('r1', 1, 2, 'pending')")

assert accept_request(db, "r1", 2) == "accepted"
assert accept_request(db, "r1", 2) == "already-processed"
assert db.execute("SELECT * FROM friendship").fetchall() == [(1, 2)]

db.execute("DELETE FROM friendship WHERE user_low = 1 AND user_high = 2")
assert accept_request(db, "r1", 2) == "already-processed"
assert db.execute("SELECT * FROM friendship").fetchall() == []
assert db.execute(
    "SELECT status FROM friend_request WHERE request_id = 'r1'"
).fetchone() == ("accepted",)

db.execute("INSERT INTO friend_request VALUES ('r2', 1, 2, 'pending')")
assert accept_request(db, "r2", 2) == "accepted"
assert db.execute("SELECT * FROM friendship").fetchall() == [(1, 2)]
db.close()
print("通过、重复提交、删除后重试与新申请检查通过")

本轮在 Python 3.12.14、SQLite 3.53.1 中实际执行了文中的建表 SQL 与两个 Python 代码块,并补充了独立边界验证,共 14 类检查通过:重复处理、删除后重试、新申请、错误处理者、拒绝态、不存在的申请、已有关系、相反方向的申请、约束检查、写入中途失败回滚以及两个连接的写锁冲突等。

中途失败测试在更新申请前人为制造数据库拒绝,确认关系插入也被回滚,申请仍为 pending。写锁测试只验证第二连接在写锁占用期间失败、释放后能重新处理,不是并发吞吐量测试,也不能推广为其他数据库隔离级别的验证。

这些检查只覆盖本地教学模型,没有验证产品客户端、真实好友接口、账号权限系统或设备间同步。

6. 接入界面时,分别显示操作结果和当前关系

申请记录中的“已通过”可以继续说明历史结果;是否显示“当前好友”,应查询当前关系。接口返回 already-processed 时,也不能由客户端直接把联系人按钮改成好友状态,应按当前关系重新读取。

这个例子只阻止已经通过的旧申请再次产生建关系副作用。删除关系时,要不要使尚未处理的旧申请失效;两边是否允许重复申请;拉黑是否阻止通过;申请是否过期,仍需另定规则。不能把未处理的旧申请改个标识就当成获得了新的授权。

同样,accepted 配合关系缺失,既可能来自后续删除,也可能来自历史数据异常。若要区分原因,需要受控的操作记录或关系生命周期信息,而不是自动回填关系。本文没有实现完整审计日志。

回到开头:一次申请“曾经通过”,不能替代“现在仍是好友”的判断。将申请历史与当前关系分开保存,让首次处理在事务中完成,再让同一申请的重试只返回历史结果,才能保留可解释的状态,并避免旧操作把已经删除的关系重新建立。

参考资料

相关文章
|
12天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
7926 15
|
10天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1737 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
11天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1734 11
|
9天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
24天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3789 10
|
19天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1990 1