Web和手持机怎样一致判断“这台设备能不能借”?

举报
yd_299226675 发表于 2026/10/28 11:23:29 2026/10/28
【摘要】 Web显示这台相机可以借,手持机提交却被拒绝。先查两端是不是各写了一套状态判断,再看服务端提交时是否依据最新状态强制校验。统一规则可以减少解释分叉,但两个人同时提交仍需数据库并发控制,不能只靠页面按钮。

Web显示这台相机可以借,手持机提交却被拒绝。先查两端是不是各写了一套状态判断,再看服务端提交时是否依据最新状态强制校验。统一规则可以减少解释分叉,但两个人同时提交仍需数据库并发控制,不能只靠页面按钮。

先列清这次操作到底依赖什么

以外借为例,至少区分“当前是否被占用”和“是否故障”。独立示例只选四种输入:空闲正常允许;空闲故障拒绝;已外借拒绝;维修占用拒绝。报废、退库、维修的规则可能不同,不要把这张外借真值表套到全部操作。

业务规则返回稳定原因码,前端再做可读提示。开发时把Web和手持机需要使用的含义写在同一处,比让两端各解释一遍“使用中”更容易保持一致。对未知状态应明确拒绝或走待确认,避免默认变成允许。

页面提前提示,提交仍需再校验

页面拿到的能力信息适合提前解释按钮为什么不可用;请求到达服务端时,必须重新加载目标状态并强制判断。AssetaGuard选定的Web与移动外借提交服务均调用了业务Guard;这不意味着每个客户端按钮都已消费同一份能力字典,也不意味着所有业务入口已经统一。

共享判定之后,还要处理同时借出

两个请求可能都读到版本7且空闲,各自校验都允许。写入时应把旧版本和空闲条件纳入数据库更新,或使用经验证的其他并发方案:

UPDATE assets
SET occupancy = 'Lent', version = version + 1
WHERE id = :id AND version = :expected_version AND occupancy = 'Idle';

只有影响一行才表示该条件写入成功;影响零行时重新读取并解释冲突。这是设计示意,不是现产品SQL。EF Core并发文档说明了并发令牌、更新条件与冲突异常;不同数据库的令牌实现应单独选定,不能把SQL Server的rowversion直接套到所有数据库。

规则一致性与并发正确性分开验证

完整独立示例让WebSubmit和MobileSubmit调用同一个LendingBoundary,再由它读取最新状态、判规则、检查展示版本并条件写入。两端先拿到同一空闲预览,Web提交成功后,Mobile再次提交会按最新状态收到OCCUPIED,而不是沿用旧按钮的允许结果。

程序还实际启动两项Task,在同一个屏障处等两笔请求都通过读后校验,再竞争带锁的内存条件写入。断言结果恰好一笔SUCCESS、一笔CONFLICT,版本只增加一次。它验证的是本进程多线程内存边界,不是数据库、EF或多实例并发测试,也不证明真实部署的吞吐量。

落到自己的业务里,应再用两个独立数据库上下文同时读取同一版本,分别提交,检查只有一条业务占用成功;规则单测与这个集成测试各负责一层。

完整独立示例

以下程序使用合成数据,不调用产品接口,不需要账号、数据库或模型服务。准备 .NET 8或兼容SDK,将项目文件与程序放入同一目录。运行命令为 dotnet run --project . -- example-output,输出文件写入指定目录。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
using System.Text.Json;
using System.Globalization;

var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Path.Combine(Environment.CurrentDirectory, "example-output");
Directory.CreateDirectory(root);
var results = new Dictionary<string, object>();
CheckPolicy();
File.WriteAllText(Path.Combine(root, "results.json"), JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
void Require(bool value, string code) { if (!value) throw new InvalidOperationException(code); }

void CheckPolicy()
{
    static string Decide(string occupancy, bool broken) => occupancy != "Idle" ? "OCCUPIED" : broken ? "BROKEN" : "ALLOWED";
    var expected = new Dictionary<(string, bool), string> { [("Idle", false)] = "ALLOWED", [("Idle", true)] = "BROKEN", [("Lent", false)] = "OCCUPIED", [("Repair", false)] = "OCCUPIED" };
    foreach (var row in expected) Require(Decide(row.Key.Item1, row.Key.Item2) == row.Value, "RULE_TABLE");
    // 两个适配入口调用同一服务;版本与占用的条件写入在本进程锁内完成。
    var store = new AssetStore(new("Idle", false, 7));
    var service = new LendingBoundary(store, Decide);
    string WebSubmit(int displayedVersion) => service.Submit(displayedVersion);
    string MobileSubmit(int displayedVersion) => service.Submit(displayedVersion);
    var webPreview = store.Read(); var mobilePreview = store.Read();
    Require(WebSubmit(webPreview.Version) == "SUCCESS", "WEB_FAILED");
    Require(MobileSubmit(mobilePreview.Version) == "OCCUPIED", "LATEST_STATE_NOT_CHECKED");
    var raceStore = new AssetStore(new("Idle", false, 7));
    var raceService = new LendingBoundary(raceStore, Decide);
    using var barrier = new Barrier(2);
    void AfterRead() { if (!barrier.SignalAndWait(TimeSpan.FromSeconds(10))) throw new TimeoutException("BARRIER_TIMEOUT"); }
    var attempts = new[] { Task.Run(() => raceService.Submit(7, AfterRead)), Task.Run(() => raceService.Submit(7, AfterRead)) };
    Task.WaitAll(attempts);
    var codes = attempts.Select(x => x.Result).OrderBy(x => x).ToArray();
    Require(codes.SequenceEqual(new[] { "CONFLICT", "SUCCESS" }) && raceStore.Read().Version == 8, "CONCURRENT_DOUBLE_COMMIT");
    results["A5"] = new { passed = true, ruleRows = 4, adapters = new[] { "Web", "Mobile" }, stalePreview = "OCCUPIED", concurrentOutcomes = codes, storage = "locked_in_process_memory", databaseTest = false };
    Console.WriteLine("PASS A5 ruleRows=4 adapters=2 concurrentSuccess=1 concurrentConflict=1 databaseTest=false");
}

public record AssetSnapshot(string Occupancy, bool Broken, int Version);
public sealed class AssetStore(AssetSnapshot initial)
{
    private readonly object gate = new();
    private AssetSnapshot current = initial;
    public AssetSnapshot Read() { lock (gate) return current; }
    public bool TryLend(int version)
    {
        lock (gate)
        {
            if (current.Version != version || current.Occupancy != "Idle" || current.Broken) return false;
            current = current with { Occupancy = "Lent", Version = version + 1 };
            return true;
        }
    }
}
public sealed class LendingBoundary(AssetStore store, Func<string, bool, string> decide)
{
    public string Submit(int displayedVersion, Action? afterRead = null)
    {
        var latest = store.Read();
        var decision = decide(latest.Occupancy, latest.Broken);
        if (decision != "ALLOWED") return decision;
        if (latest.Version != displayedVersion) return "CONFLICT";
        afterRead?.Invoke();
        return store.TryLend(latest.Version) ? "SUCCESS" : "CONFLICT";
    }
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。