项目概览
PhoneNumber 是一套已经投入生产使用的手机号码选号与订单平台。访客可以按归属信息、运营商、价格区间和号码规律筛选库存;注册用户可以管理购物车、收货地址和订单,并通过在线支付完成交易;运营人员则通过独立管理入口维护号码、套餐、订单、支付、邮件、客服消息和站点合规信息。
它并不是一个只有搜索页面的静态目录。号码库存具有唯一性和已售状态,支付回调会改变商品可见性,用户消息需要在前台与管理台之间实时同步,账号与支付配置还涉及密钥、Cookie、邮件验证码和个人信息保护。系统必须同时处理商品目录、身份认证、交易状态、运营配置和生产部署。
本文以当前源码提交 d7d772a 为代码分析基线,项目投入生产使用是已经确认的事实。仓库没有发布标签或部署 manifest,因此本文不会声称该提交就是现场安装版本,也不会把当前验证结果外推为长期运行指标。
匿名化与公开边界
本文保留号码目录、订单、支付和客服等通用业务概念,但不会公开:
- 生产站点品牌、公司、运营主体和合作方信息;
- 真实域名、服务器地址、端口、部署路径和数据库连接信息;
- 真实手机号码、用户账号、收货地址、订单或支付流水;
- 支付应用标识、私钥、公钥、回调地址和签名参数样本;
- SMTP 主机、邮箱、密码、验证码和邮件正文样本;
- 管理员账号、初始密码、Cookie、Data Protection Key 和会话文件;
- 真实价格、套餐内容、销售量、转化率和未经记录的可用性指标;
- 源码仓库地址、生产截图和可用于还原现场配置的文件内容。
下文中的架构和流程均为脱敏后的工程说明,不能用于定位或访问生产系统。
项目目标
系统围绕“号码可发现、订单可闭环、运营可维护”建立以下目标:
- 将本地号码库存组织为可搜索、可筛选和可分页的公开目录;
- 支持号码规律识别、价格排序、套餐说明和已售商品下架;
- 为用户提供注册、登录、邮箱验证、购物车、地址和订单管理;
- 以服务端库存价格创建订单,避免直接信任浏览器提交的金额;
- 接入在线支付的表单跳转、异步通知、同步返回和主动查询;
- 通过订单与号码状态联动降低同一号码被重复售出的风险;
- 用 SignalR、站内消息和邮件通知连接用户与运营人员;
- 让管理员能够维护号码、套餐、订单、支付、邮件和法律信息;
- 通过 MySQL 迁移、nginx 静态托管和反向代理完成生产交付;
- 用后端与前端测试验证核心服务和客户端接口行为。
技术栈与职责
| 层次 | 技术与组件 | 主要职责 |
|---|---|---|
| 用户前台 | 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、健康回滚和数据库恢复演练。
后续路线
- 立即删除支付私钥与支付表单调试日志,审计历史日志并轮换支付密钥;
- 为支付发起接口增加订单归属检查,为回调增加金额、应用和收款方校验;
- 通过唯一占用记录、条件更新或数据库锁保证同一号码只能支付成功一次;
- 对不存在或价格无效的库存拒绝创建订单,并为订单状态建立明确状态机;
- 增加 CSRF Token、Secure Cookie、统一限流、登录锁定和安全响应头;
- 密码变更与重置后旋转 SessionStamp,并支持主动注销全部设备;
- 将 Data Protection Key、支付密钥、SMTP 密码和初始管理员凭据迁出源码与业务表;
- 清理仓库中的运行密钥和会话文件,完成 Git 历史审计与凭据轮换;
- 修复测试工程、锁文件和 ESLint,建立后端、前端与安全扫描 CI;
- 增加支付并发、回调重放、权限越界、页面流程和 SignalR 集成测试;
- 将号码规律转换为可索引特征或受控查询,建立性能基线;
- 用发布 manifest 关联源码、依赖、迁移、前后端产物和生产版本;
- 建立数据库备份恢复、版本化发布、健康检查和自动回滚演练。
项目复盘
这个项目的核心价值,是把号码库存从一组静态数据变成了可运营的完整业务系统:
- 目录层负责发现、筛选、规律识别和已售下架;
- 用户层负责账号、地址、购物车、订单与消息;
- 交易层负责服务端定价、支付验签、状态同步和履约;
- 运营层负责库存、套餐、订单、客服和站点配置;
- 数据层用 MySQL 维持身份、商品、交易和会话事实;
- 部署层将 React 静态资源与 ASP.NET Core API 统一到生产入口。
生产使用证明了这条业务链能够承担真实操作,也使安全与一致性问题更加重要。下一阶段的首要目标不是继续增加页面,而是关闭支付授权和密钥日志风险,恢复可复现构建与测试门禁,并把订单唯一性、凭据治理、审计和回滚变成系统约束。