项目概览

PhoneNumber 是一套已经投入生产使用的手机号码选号与订单平台。访客可以按归属信息、运营商、价格区间和号码规律筛选库存;注册用户可以管理购物车、收货地址和订单,并通过在线支付完成交易;运营人员则通过独立管理入口维护号码、套餐、订单、支付、邮件、客服消息和站点合规信息。

它并不是一个只有搜索页面的静态目录。号码库存具有唯一性和已售状态,支付回调会改变商品可见性,用户消息需要在前台与管理台之间实时同步,账号与支付配置还涉及密钥、Cookie、邮件验证码和个人信息保护。系统必须同时处理商品目录、身份认证、交易状态、运营配置和生产部署。

本文以当前源码提交 d7d772a 为代码分析基线,项目投入生产使用是已经确认的事实。仓库没有发布标签或部署 manifest,因此本文不会声称该提交就是现场安装版本,也不会把当前验证结果外推为长期运行指标。

匿名化与公开边界

本文保留号码目录、订单、支付和客服等通用业务概念,但不会公开:

  • 生产站点品牌、公司、运营主体和合作方信息;
  • 真实域名、服务器地址、端口、部署路径和数据库连接信息;
  • 真实手机号码、用户账号、收货地址、订单或支付流水;
  • 支付应用标识、私钥、公钥、回调地址和签名参数样本;
  • SMTP 主机、邮箱、密码、验证码和邮件正文样本;
  • 管理员账号、初始密码、Cookie、Data Protection Key 和会话文件;
  • 真实价格、套餐内容、销售量、转化率和未经记录的可用性指标;
  • 源码仓库地址、生产截图和可用于还原现场配置的文件内容。

下文中的架构和流程均为脱敏后的工程说明,不能用于定位或访问生产系统。

项目目标

系统围绕“号码可发现、订单可闭环、运营可维护”建立以下目标:

  1. 将本地号码库存组织为可搜索、可筛选和可分页的公开目录;
  2. 支持号码规律识别、价格排序、套餐说明和已售商品下架;
  3. 为用户提供注册、登录、邮箱验证、购物车、地址和订单管理;
  4. 以服务端库存价格创建订单,避免直接信任浏览器提交的金额;
  5. 接入在线支付的表单跳转、异步通知、同步返回和主动查询;
  6. 通过订单与号码状态联动降低同一号码被重复售出的风险;
  7. 用 SignalR、站内消息和邮件通知连接用户与运营人员;
  8. 让管理员能够维护号码、套餐、订单、支付、邮件和法律信息;
  9. 通过 MySQL 迁移、nginx 静态托管和反向代理完成生产交付;
  10. 用后端与前端测试验证核心服务和客户端接口行为。

技术栈与职责

层次 技术与组件 主要职责
用户前台 React 19、TypeScript 6、Vite 号码查询、购物车、支付、订单、地址、消息和账户
运营后台 React、管理页面 号码、Excel 导入、套餐、订单、客服、支付和邮件配置
HTTP API .NET 9、ASP.NET Core Controllers 输入校验、认证授权、业务调用和统一错误响应
实时通信 ASP.NET Core SignalR 用户与管理员消息推送、分组和断线重连
业务服务 Catalog、Order、Auth、Payment、Message Services 目录、身份、交易、支付和客服用例
数据访问 EF Core 9、Pomelo、MySQL 库存、用户、订单、配置、消息和迁移
身份安全 Cookie Authentication、PasswordHasher、Data Protection 登录态、密码哈希、角色策略和 Cookie 保护
外部集成 在线支付 SDK、SMTP、MailKit 支付创建/验签/查询、验证码和运营通知
数据导入 ClosedXML 批量导入与更新号码库存
验证交付 xUnit、Vitest、ESLint、nginx 服务测试、客户端测试、静态构建和生产入口

后端采用 Controller → Service → EF Core 的分层方式。前端通过集中 API 客户端调用 /api,所有请求携带 Cookie;SignalR 使用同一登录态连接聊天 Hub。nginx 负责静态资源与后端反向代理,使前端无需感知生产内部地址。

总体架构

┌──────────────────────────── 浏览器 ────────────────────────────┐
│ React 用户前台:目录 / 购物车 / 支付 / 订单 / 地址 / 消息       │
│ React 管理后台:库存 / 套餐 / 订单 / 客服 / 支付 / 邮件 / 合规  │
└──────────────────────────────┬─────────────────────────────────┘
                               │ HTTPS
                               ▼
┌──────────────────────────── nginx ─────────────────────────────┐
│ Vite 静态产物                         /api 与 /hubs 反向代理      │
└──────────────────────────────┬─────────────────────────────────┘
                               ▼
┌────────────────────── ASP.NET Core / .NET 9 ───────────────────┐
│ Controllers / Cookie Auth / AdminOnly / SignalR                │
│        │                                                       │
│ Catalog / Auth / Cart / Order / Payment / Message Services     │
│        │                         │                              │
│ EF Core / MySQL                 ├─ 在线支付平台                 │
│                                  └─ SMTP 邮件服务               │
└────────────────────────────────────────────────────────────────┘

目录浏览允许匿名访问,购物车、订单、地址和消息要求登录,管理接口要求管理员角色。支付异步通知是外部平台调用的匿名入口,因此不能依赖用户 Cookie,只能依赖签名验证和订单数据校验。

后端启动与请求链路

后端启动时加载 NLog、配置、Controller、OpenAPI、SignalR、CORS、Data Protection、Cookie Authentication 和授权策略。数据库连接使用 EF Core MySQL Provider,应用启动阶段自动执行迁移,并在数据库中不存在管理员时按部署配置初始化管理账号。

启动进程
  → 加载配置与 NLog
  → 注册 Controller、SignalR、认证和业务服务
  → 建立 EF Core / MySQL 上下文
  → 自动执行数据库迁移
  → 确认管理员账号
  → 启用异常处理、CORS、认证和授权
  → 映射 API、健康检查与 Chat Hub

全局异常处理把业务校验、资源不存在、认证失败和未知异常转换为一致的 JSON 错误,同时记录请求方法、路径和状态码。开发环境开放 OpenAPI,生产环境只保留业务 API、健康检查和 SignalR Hub。

号码目录与动态筛选

号码目录以 MySQL 中的本地库存为事实来源。查询默认排除已售号码,并支持关键词、归属信息、运营商、最低/最高价格、页码和每页数量。结果按价格和号码排序,关联套餐摘要与有效期后返回前端。

筛选元数据采用“排除自身条件”的方式计算:请求归属地列表时保留其他过滤条件但忽略当前归属地,请求运营商列表时采用相同策略。这能避免联动筛选把仍然有效的选择提前移除。

号码规律支持重复数字、连续升降序、成对重复和交替组合等类型。当前实现会先从数据库读取所有满足普通条件的候选项,再在应用内存中完成规律匹配和分页。这个选择便于实现复杂模式,但库存增长后会放大数据库传输、内存占用和响应时间,需要改造为可索引特征或数据库侧查询。

库存、Excel 导入与套餐

管理端提供号码的分页查询、新增、修改、删除和 Excel 导入。数据库对号码建立唯一索引,单条新增会先执行重复检查;Excel 导入识别固定表头,对合法号码进行新增或更新,并跳过不符合格式的数据。

上传 Excel
  → 验证文件类型
  → 读取首个工作表与表头
  → 逐行规范化号码、归属、运营商和价格
  → 查询已存在号码
  ├─ 已存在:更新运营信息和价格
  └─ 不存在:新增库存记录
  → 保存并返回新增/更新统计

套餐作为独立实体维护名称、摘要、详情和有效期,可以绑定单个号码,也可以批量绑定或解绑。删除套餐时先解除关联号码,再删除套餐本身,避免目录记录因套餐生命周期结束而被删除。

用户账号与登录体系

用户可以使用用户名、手机号或邮箱配合密码登录,也可以使用邮箱验证码登录。密码通过 ASP.NET Core PasswordHasher 保存;忘记密码流程只把随机令牌的 SHA-256 摘要和过期时间写入数据库,邮件发送原始令牌。

Cookie 设置为 HttpOnly 和 SameSite=Lax。登录时生成新的 SessionStamp 并写入 Cookie Claim;服务端在验证 Cookie 时比较数据库中的 Stamp,使后一次登录能够使前一次带 Stamp 的会话失效。管理员接口通过角色 Claim 和 AdminOnly 策略保护。

用户还可以:

  • 注册并选择性绑定手机号和邮箱;
  • 通过邮件验证码绑定或更换邮箱;
  • 修改用户名、手机号和密码;
  • 开启登录算术验证码;
  • 使用邮件令牌重置密码;
  • 查看用户协议和隐私政策。

邮箱验证码存放在单进程内存中并设置发送冷却和有效期。它适合当前单实例应用,但服务重启会丢失验证码,多实例也不能共享状态。

购物车、地址与订单

购物车对“用户 + 号码”建立唯一约束,重复加入返回已有项目。收货地址按用户隔离,支持新增、编辑、删除和默认地址切换。订单创建时保存买家、地址和号码快照,使后续库存信息变化不会覆盖历史订单内容。

浏览可售号码
  → 登录或注册
  → 加入购物车 / 直接选择
  → 选择或填写收货地址
  → 服务端重新读取号码与价格
  → 创建待处理、未支付订单
  → 发送运营通知
  → 用户发起在线支付

同一用户对同一号码的未取消订单会被应用层拦截,已售号码也会在创建订单时再次检查。订单状态支持待审核、已联系、已发货、已完成和已取消,支付状态与履约状态分别保存;管理员可以维护备注、物流公司和物流单号。

服务端价格读取是正确方向,但当前在找不到匹配库存时会回退为零值而不是拒绝创建订单。订单去重也缺少数据库唯一约束,必须在后续改造中把“库存存在、价格有效、同用户重复订单”变成数据库和事务层面的硬约束。

在线支付与号码下架

支付服务负责生成电脑网站支付表单,处理支付平台异步通知、同步返回签名以及主动查询。支付回调通过平台公钥验证签名,只有明确的成功状态才会更新订单。

用户发起支付
  → 读取服务端订单金额
  → 生成签名支付表单
  → 浏览器跳转支付平台
  ├─ 异步通知:验签 → 更新支付状态
  ├─ 同步返回:验签 → 更新支付状态
  └─ 主动查询:查询交易 → 补偿页面未及时收到的状态
        ↓
  标记号码已售并从公开目录下架
  → 将同号码其他未支付订单标记为被他人购买

异步通知、同步返回和主动查询都具有重复调用场景,代码会先检查当前支付状态,降低重复更新风险。号码变为已售后,目录查询会自动排除它。

当前支付一致性仍主要依赖应用层“先查后改”。订单、库存和竞争订单在一次 SaveChanges 中提交,但数据库没有“同一号码只能有一个成功购买者”的唯一约束或并发令牌;两个支付处理并发进入时仍存在竞争窗口。生产交易系统需要数据库锁、唯一占用记录或条件更新来保证最终唯一性。

实时客服与邮件通知

登录用户可以发送站内消息,管理员可以在后台查看会话列表、未读数量和消息历史并回复。消息先写入 MySQL,再通过 SignalR 推送:用户连接加入自己的分组,管理员连接额外加入管理员分组。

用户消息
  → MessageService 持久化
  → SignalR 推送管理员分组
  → 可选发送运营通知邮件

管理员回复
  → MessageService 持久化
  → SignalR 推送目标用户分组
  → 可选发送用户邮件提醒

前端使用自动重连并在重新连接后刷新当前会话和会话列表;未读消息还通过周期查询补偿实时连接中断。分页、已读状态和邮件通知让客服功能不完全依赖浏览器长连接。

管理后台与运营配置

管理页面集中承载以下能力:

  • 号码库存的搜索、增删改和 Excel 导入;
  • 套餐创建、维护、删除及单个/批量绑定;
  • 订单筛选、搜索、状态、备注和物流维护;
  • 用户会话列表、实时消息和邮件提醒;
  • 支付参数保存、启停和连接测试;
  • SMTP 参数、收件人和通知开关维护;
  • 站点名称、备案、联系方式、用户协议和隐私政策维护;
  • 管理员资料、邮箱、密码和登录验证码设置。

配置读取接口不会把支付私钥或 SMTP 密码返回前端,空白密钥字段表示保留现值。这减少了管理页面回显敏感信息,但数据库中的支付私钥和 SMTP 密码目前仍是可直接使用的原始值,缺少应用层加密与外部密钥管理。

MySQL、迁移与数据模型

EF Core 数据模型覆盖:

  • 用户账号、密码摘要、邮箱、会话 Stamp 和管理员标志;
  • 手机号码、归属、运营商、价格、套餐和已售状态;
  • 购物车、收货地址和订单快照;
  • 支付状态、支付流水、履约状态和物流信息;
  • 用户与管理员消息、已读状态和时间线;
  • 支付、SMTP 与法律信息配置。

用户标识、手机号、邮箱和库存号码具有唯一索引,购物车也具有组合唯一索引。应用启动自动执行迁移,降低人工漏执行 schema 变更的概率,但启动即迁移也意味着应用账号需要较高数据库权限,生产环境应评估拆分迁移身份和运行身份。

当前模型没有通用审计日志、软删除、数据保留任务、乐观并发版本或订单支付事件表。订单、地址、消息和支付流水包含个人信息,需要建立备份、访问审计、保留期限和删除流程。

React 前端与交互组织

前端使用 Vite 构建单页应用,没有引入 React Router 或全局状态库。顶层 App 通过 History API 区分用户入口和管理入口,并用本地状态切换首页、消息、购物车和个人中心。底部导航面向移动端,桌面布局复用同一组件树。

API 客户端统一处理 Cookie、JSON 和错误响应;目录筛选元数据缓存在 LocalStorage,搜索条件变化后做短延迟刷新。支付完成后,首页读取回跳参数并引导用户查看订单;SignalR 和周期查询共同维护消息状态。

这种轻量设计适合当前页面规模,但随着路由、权限和数据依赖增加,手写 History 与页面状态容易产生刷新恢复、深链接和状态同步问题。当前 ESLint 也已经发现 Effect 内同步更新、Hook 依赖和函数声明时序等问题,需要在继续扩展前整理。

nginx 与生产交付

仓库提供 nginx 部署说明,推荐将 Vite dist 作为静态前端,并把 API 与 SignalR 请求反向代理到 ASP.NET Core 服务。后端通过独立进程管理方式运行,MySQL、支付和 SMTP 参数由部署环境提供。

后端 Release 发布
  → 前端锁定依赖并执行 Vite 构建
  → 数据库备份与迁移评审
  → 上传后端与静态产物
  → 重启应用服务
  → nginx 配置验证与平滑重载
  → 验证健康检查、登录、目录、支付回调和 SignalR

项目已经确认用于生产,但仓库没有发布标签、统一 manifest、CI 工作流、自动部署或回滚脚本。本文只能确认源码中存在部署说明,不能据此推断当前服务器的操作系统、反向代理细节或实际目录。

构建与测试基线

本案例编写前在当前工作区重新执行了构建与测试:

验证项 结果与边界
代码分析基线 d7d772a,master 与远端一致;仅存在未跟踪 CodeGraph 索引
发布标签 当前没有 Git 标签,无法把源码提交与现场版本严格对齐
后端应用 Release 构建 1 个项目,0 error、0 warning
完整 .NET 解决方案 测试替身未实现新增邮件接口,1 个编译错误,测试无法运行
前端生产构建 TypeScript 与 Vite 构建成功
前端测试 2 个测试文件、11 项 Vitest 全部通过
前端依赖复现 package.json 与锁文件不同步,npm ci 失败
前端 ESLint 9 个错误、8 个警告
持续集成 当前没有 GitHub Actions 工作流

前端测试主要覆盖 API 客户端和目录参数逻辑,没有覆盖页面、支付回跳、SignalR、购物车和管理后台。后端已有目录、认证、订单、消息、通知和数据库启动测试源码,但当前测试工程无法编译,所以不能声称这些测试在此基线上通过。

生产使用结果

根据已经确认的生产事实,可以做出以下定性描述:

  • 号码目录、筛选、规律匹配和已售下架已经用于真实业务;
  • 用户账号、购物车、地址、订单和在线支付形成了完整交易入口;
  • 管理后台支持库存、套餐、订单、客服和运行配置维护;
  • SignalR 与邮件通知连接用户咨询和运营处理;
  • MySQL 迁移、前后端构建和 nginx 部署说明形成了基础交付链;
  • 当前源码仍能分别完成后端应用与前端生产构建。

本文不公开真实用户、号码和交易数据,也不虚构访问量、订单量、支付成功率、响应时间或可用性指标。生产使用事实不等同于当前提交、依赖锁文件和测试工程已经达到可复现发布标准。

已知边界与风险

支付与订单一致性

  • 发起支付接口读取订单时没有校验订单归属于当前登录用户,存在横向授权边界缺口;
  • 回调完成签名验证,但没有显式核对通知金额、应用身份和收款方身份与本地订单一致;
  • 支付调试日志会记录规范化私钥和支付表单内容,属于必须立即移除并轮换相关密钥的高风险问题;
  • 同一号码支付成功依赖应用层先查后改,没有唯一占用约束、并发令牌或条件更新;
  • 订单创建在库存不存在时可能使用零值价格,而不是拒绝无效商品;
  • 支付状态、履约状态和库存状态缺少独立事件流水与补偿任务,异常恢复依赖日志和人工判断。

身份认证与 Web 安全

  • Cookie API 没有显式的 Antiforgery/CSRF Token 机制,SameSite=Lax 不能替代完整防护;
  • Cookie 没有在代码中明确固定 Secure、有效期和滑动策略,需要结合反向代理和生产配置审计;
  • 登录、注册、找回密码和验证码接口缺少统一限流、失败锁定和滥用检测;
  • 算术验证码允许回退到代码内默认密钥,部署遗漏配置时安全性不足;
  • 邮箱验证码保存在单机内存,重启丢失且不能支持多实例;
  • 修改或重置密码后没有旋转 SessionStamp,其他现有会话不一定立即失效。

密钥、配置与隐私

  • 仓库当前跟踪运行配置文件、Data Protection Key 和代理会话类文件,需要立即审计、从历史中清除并轮换受影响凭据;
  • 支付私钥和 SMTP 密码以数据库可直接使用的形式保存,没有外部密钥管理或应用层加密;
  • 订单通知邮件包含号码、联系人和地址信息,邮件、日志和数据库缺少统一脱敏与保留策略;
  • 用户、地址、订单、消息和支付数据没有明确的数据导出、删除、归档与审计流程;
  • 管理操作缺少独立审计日志,无法完整回答谁在何时修改了库存、价格、订单或配置。

构建、测试与前端质量

  • npm ci 因锁文件不一致失败,干净环境不能可靠复现前端依赖;
  • 后端测试工程无法编译,完整解决方案不能通过质量门禁;
  • ESLint 存在 9 个错误和 8 个警告,集中在 Effect、Hook 依赖和类型定义;
  • 前端测试仅覆盖 11 个 API/目录用例,关键页面和实时通信缺少自动化回归;
  • 号码规律在应用内存中匹配,库存增长后可能成为性能瓶颈;
  • 仓库没有 CI、版本标签、部署 manifest、健康回滚和数据库恢复演练。

后续路线

  1. 立即删除支付私钥与支付表单调试日志,审计历史日志并轮换支付密钥;
  2. 为支付发起接口增加订单归属检查,为回调增加金额、应用和收款方校验;
  3. 通过唯一占用记录、条件更新或数据库锁保证同一号码只能支付成功一次;
  4. 对不存在或价格无效的库存拒绝创建订单,并为订单状态建立明确状态机;
  5. 增加 CSRF Token、Secure Cookie、统一限流、登录锁定和安全响应头;
  6. 密码变更与重置后旋转 SessionStamp,并支持主动注销全部设备;
  7. 将 Data Protection Key、支付密钥、SMTP 密码和初始管理员凭据迁出源码与业务表;
  8. 清理仓库中的运行密钥和会话文件,完成 Git 历史审计与凭据轮换;
  9. 修复测试工程、锁文件和 ESLint,建立后端、前端与安全扫描 CI;
  10. 增加支付并发、回调重放、权限越界、页面流程和 SignalR 集成测试;
  11. 将号码规律转换为可索引特征或受控查询,建立性能基线;
  12. 用发布 manifest 关联源码、依赖、迁移、前后端产物和生产版本;
  13. 建立数据库备份恢复、版本化发布、健康检查和自动回滚演练。

项目复盘

这个项目的核心价值,是把号码库存从一组静态数据变成了可运营的完整业务系统:

  • 目录层负责发现、筛选、规律识别和已售下架;
  • 用户层负责账号、地址、购物车、订单与消息;
  • 交易层负责服务端定价、支付验签、状态同步和履约;
  • 运营层负责库存、套餐、订单、客服和站点配置;
  • 数据层用 MySQL 维持身份、商品、交易和会话事实;
  • 部署层将 React 静态资源与 ASP.NET Core API 统一到生产入口。

生产使用证明了这条业务链能够承担真实操作,也使安全与一致性问题更加重要。下一阶段的首要目标不是继续增加页面,而是关闭支付授权和密钥日志风险,恢复可复现构建与测试门禁,并把订单唯一性、凭据治理、审计和回滚变成系统约束。