PAYATHON 2026

Web based 还是通过 VPN 连接的 Thick-client?

支付老李

结论

建议采用”中心化 Web 系统 + HTTPS API”,不要让桌面客户端通过 VPN 直接连接 MySQL。

如果部分服务中心经常断网,可以在 Web 架构中加入离线录入和断点同步功能。也可以配套开发轻量级 VB.NET 离线客户端,但客户端只能调用业务 API,不能直接操作中心数据库。

推荐结构如下:

服务中心浏览器 / 离线客户端
          │
       HTTPS
          │
   Web 应用与业务 API
          │
   内网中的 MySQL 数据库
          │
  账单生成、PDF 下载与打印

VPN 可以用于服务器运维、内部管理或额外的网络隔离,但不适合作为每台业务终端访问数据库的主要方式。

为什么不建议 Thick-client 直连数据库

桌面客户端通过 VPN 直接连接 MySQL,看起来开发简单,却会把大量问题转移到客户端和网络上。

数据库连接对网络质量比较敏感。移动网络短暂断线、切换基站或延迟升高时,正在执行的事务可能失败。客户端很难判断某条读数是”没有提交”,还是”已经提交,但响应丢失”,容易产生重复数据。

采用这种方案,数据库还必须接受各服务中心的连接。即使有 VPN,也要在大量终端中保存数据库连接信息,并处理账号权限、驱动版本、连接池和证书等问题。一旦某台终端被恶意软件控制,攻击者可能获得超出正常业务范围的数据库访问能力。

部署和维护同样麻烦。计费规则、校验逻辑或数据库结构每次发生变化,都可能需要升级全部客户端。某个服务中心未及时更新时,系统中就会同时运行多个版本。

Web/API 架构可以把计费规则、权限检查、审计和数据库访问集中到服务器端。客户端只需提交”录入读数""查询账单”等业务请求,不必了解数据库表结构。

网络稳定性应该怎样评估

不能只确认”当地可以上网”,还要按照实际业务条件测试。建议至少记录以下指标:

  • 工作时段的断线次数和每次断线的持续时间;
  • 上传延迟、丢包率,以及移动网络切换后的恢复时间;
  • 每个服务中心每天录入的电表数量;
  • 单次提交的数据量;
  • 网络中断时允许积压的时间;
  • 月末集中出账时的并发量。

电表读数的数据量通常不大,带宽往往不是主要问题。更需要关注的是连接能否保持稳定,以及失败后能否安全重试。

如果多数服务中心的网络稳定,可以使用普通 Web 表单,并允许在浏览器中暂存尚未提交的内容。如果经常连续断网数小时,就需要真正的离线队列,在本地保存记录,联网后自动同步,同时清楚显示”未同步、同步中、成功、冲突”等状态。

不能把”提交失败,请重新点击”当成同步方案。网络故障可能发生在服务器已经保存数据、客户端却还没有收到响应的时候,因此每次提交都应带有全局唯一的业务编号。

推荐的实施步骤

1. 先统一业务数据

每块电表都必须有稳定且唯一的编号。一次读数至少应包含:

  • ReadingId:客户端生成的唯一提交编号;
  • MeterId:电表编号;
  • BillingPeriod:计费周期;
  • ReadingValue:本次读数;
  • ReadAt:实际抄表时间;
  • OperatorId:录入人员;
  • BranchId:服务中心;
  • CreatedAt:本地创建时间;
  • ReceivedAt:服务器接收时间。

服务器还要检查读数是否小于上期读数、是否出现异常跳变,以及是否重复录入。异常数据不宜直接删除,可以转入复核流程。

2. 建立服务端 API

客户端不应提交任意 SQL,而应调用职责清晰的接口,例如:

POST /api/meter-readings
GET  /api/meters/{meterId}/readings/latest
GET  /api/bills/{billId}/pdf
POST /api/sync/meter-readings

API 负责身份认证、权限检查、数据校验、事务处理和审计记录。MySQL 只允许 Web/API 服务器访问,不直接向服务中心终端开放。

3. 使用幂等提交防止重复数据

可以在数据库中为业务编号设置唯一约束,同时限制同一块电表在同一计费周期内只能有一条有效读数:

CREATE TABLE meter_reading (
    reading_id CHAR(36) NOT NULL,
    meter_id BIGINT NOT NULL,
    billing_period DATE NOT NULL,
    reading_value DECIMAL(14, 3) NOT NULL,
    read_at DATETIME NOT NULL,
    operator_id BIGINT NOT NULL,
    branch_id BIGINT NOT NULL,
    created_at DATETIME NOT NULL,
    received_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (reading_id),
    UNIQUE KEY uq_meter_period (meter_id, billing_period)
);

这里假设每块电表在每个计费周期只保留一条正式读数。如果业务允许补录、复核和更正,就要另外设计版本号或更正记录,不能直接覆盖原始数据。

4. 离线客户端只通过 API 同步

如果使用 VB.NET 编写离线录入程序,可以先在本机数据库中保存待同步记录,再通过 HttpClient 上传:

Imports System.Net.Http
Imports System.Net.Http.Headers
Imports System.Text
Imports System.Text.Json

Public Class MeterReadingRequest
    Public Property ReadingId As Guid
    Public Property MeterId As Long
    Public Property BillingPeriod As Date
    Public Property ReadingValue As Decimal
    Public Property ReadAt As DateTime
    Public Property OperatorId As Long
    Public Property BranchId As Long
    Public Property CreatedAt As DateTime
End Class

Public Async Function UploadReadingAsync(
    apiBaseUrl As String,
    accessToken As String,
    reading As MeterReadingRequest
) As Task(Of Boolean)

    Using client As New HttpClient()
        client.BaseAddress = New Uri(apiBaseUrl)
        client.DefaultRequestHeaders.Authorization =
            New AuthenticationHeaderValue("Bearer", accessToken)

        Dim json As String = JsonSerializer.Serialize(reading)
        Using content As New StringContent(json, Encoding.UTF8, "application/json")
            Dim response As HttpResponseMessage =
                Await client.PostAsync("api/meter-readings", content)

            If response.IsSuccessStatusCode Then
                Return True
            End If

            Dim errorBody As String = Await response.Content.ReadAsStringAsync()
            Throw New HttpRequestException(
                $"Upload failed: {CInt(response.StatusCode)} {errorBody}")
        End Using
    End Using
End Function

本地记录只有在收到服务器的成功响应后,才能标记为”已同步”。如果发生超时或临时网络错误,应保留原来的 ReadingId 重试,不能生成新编号。

这段示例省略了令牌刷新、超时策略、重试退避和本地数据库事务,生产环境中必须补齐这些部分。对于 400、401、403 等错误,也不能无限重试。通常只有超时、连接失败和部分 5xx 错误适合自动重试。

数据冲突和同步规则

离线同步中最难处理的通常不是上传,而是冲突。例如,同一块电表可能被两个服务中心分别录入。系统需要提前确定以下规则:

  • 哪个服务中心拥有该电表的录入权限;
  • 重复读数应当拒绝、覆盖,还是交给主管复核;
  • 已出账的读数能否修改;
  • 修改后是否需要重新计算账单;
  • 谁可以批准更正;
  • 原始值和修改记录需要保留多久。

建议以服务器数据为最终结果。客户端可以保存待同步数据,但不能自行决定最终账单。每次修改都要记录操作人、操作时间、修改前后的值和修改原因。

账单下载与打印

账单应由服务器按照统一模板生成,优先使用 PDF 格式。这样可以减少因不同电脑的字体、打印驱动和页面设置而造成的金额、分页或表格错位。

服务中心通常按以下流程操作:

  1. 查询某个计费周期或客户的账单;
  2. 从服务器下载固定版本的 PDF;
  3. 在本地预览并打印;
  4. 记录下载、打印或重新打印的操作日志。

批量打印时,可以由服务器生成包含多张账单的 PDF,也可以建立受控的打印队列。核心计费逻辑不应放在浏览器 JavaScript 或各分支客户端中,否则不同版本可能计算出不同金额。

如果需要在断网时打印,可以缓存已经正式生成的 PDF,但文件必须包含账单编号、生成时间和版本标识。账单更正后,旧文件要明确标记为作废,防止重复发放。

安全方面的最低要求

无论是否使用 VPN,至少都要满足以下要求:

  • 全程使用 HTTPS;
  • 使用个人账号,禁止多人共用数据库账号;
  • 按照服务中心和岗位限制数据范围;
  • 数据库不能直接暴露到互联网;
  • 审计登录、读数录入、更正、出账和账单重打等操作;
  • 定期备份服务器和数据库,并实际演练恢复流程;
  • 对重要操作设置审批或二次确认;
  • 在服务端校验输入,并使用参数化查询;
  • 设置会话过期、密码策略和必要的多因素认证。

VPN 只能保护网络通道,不能代替应用权限、审计、输入校验和数据库最小权限。

什么时候可以考虑纯桌面方案

如果所有服务中心都需要长期离线,只能偶尔传送批量文件,或者必须操作特殊的本地硬件,可以考虑采用桌面程序。但同步仍应通过上传经过验证和签名的数据包或调用 API 完成,不能让客户端直接连接中心 MySQL。

当前场景中的服务中心具备固定网络或移动网络,还需要集中录入、下载和打印账单,因此 Web/API 更适合作为主架构。网络较差的网点可以增加离线录入组件,在集中维护和现场可用性之间取得平衡。

备注:内容仅供参考。