O2O 项目笔记

该项目采用 Spring Cloud 微服务与 OpenResty 动静分离架构。OpenResty 直接提供静态页面,将动态接口请求转发到 Gateway,再由网关路由到后端服务。项目启动、配置和故障排查应围绕请求链路进行。

系统架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flowchart LR
U[浏览器] --> O[OpenResty]
O -->|静态资源| W[Web 目录]
O -->|动态请求| G[Gateway]
G --> A[Account]
G --> N[Notice]
G --> R[Order]
G --> P[Payment]
G --> S[Storage]
G --> T[Stroke]
A --> D[(数据库与基础设施)]
N --> D
R --> D
P --> D
S --> D
T --> D

模块职责

模块 类型 主要职责 是否独立启动
account 服务 注册、登录与个人信息维护
gateway 服务 统一入口、路由和横切治理
notice 服务 站内消息收发
order 服务 订单创建与状态维护
payment 服务 支付记录和支付状态维护
storage 服务 业务数据读写封装
stroke 服务 行程信息维护
commons 依赖包 通用工具与基础能力
modules 依赖包 PO、VO 和共享模型
web 静态项目 H5 页面与前端资源 由 OpenResty 提供

启动前检查

  1. 核对各服务配置文件中的数据库、缓存、注册中心和消息服务地址。
  2. 密码与令牌应通过环境变量或密钥管理服务注入,不应直接写入仓库。
  3. 确认端口不冲突,并检查基础设施健康状态。
  4. 先构建 commonsmodules,再构建依赖它们的服务。
  5. 记录本地环境与虚拟机环境之间的域名和端口映射。

后端启动顺序

建议先启动注册中心与数据库等基础设施,再启动业务服务,最后启动 Gateway。IDEA 中可使用组合运行配置统一启动以下 Spring Boot 应用:

1
2
3
4
5
6
7
account
gateway
notice
order
payment
storage
stroke

验证时先访问各服务健康检查接口,再通过 Gateway 访问业务接口。只验证进程存在并不能证明依赖连接和路由配置正确。

前端与 OpenResty

nginx.conf 中静态目录应指向项目的 web 目录,接口路径则转发到 Gateway。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 8080;
server_name localhost;

root D:/workspace/o2o/web;
index index.html;

location /api/ {
proxy_pass http://127.0.0.1:9000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location / {
try_files $uri $uri/ /index.html;
}
}

修改配置后应先执行语法检查,再重载服务。静态页面正常但接口失败时,优先检查 /api/ 路径改写、Gateway 端口和跨域配置。

修改密码流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant U as 用户
participant A as Account 服务
participant D as 用户数据库
U->>A: 提交旧密码与新密码
A->>A: 校验登录身份与输入格式
A->>D: 按用户 ID 查询密码摘要
D-->>A: 返回摘要和账户状态
A->>A: 验证旧密码
A->>A: 检查新旧密码不同及强度规则
A->>A: 使用随机盐计算新摘要
A->>D: 条件更新密码摘要与版本
A->>A: 使现有会话失效并记录审计日志
A-->>U: 返回修改结果

密码不能使用 MD5 或普通快速哈希存储。应使用 Argon2id、bcrypt 或 scrypt 等密码哈希算法,由成熟库生成独立随机盐。接口响应不应区分用户不存在与旧密码错误,以减少账户信息泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Transactional
public void changePassword(long userId, String oldPassword, String newPassword) {
User user = userRepository.findByIdForUpdate(userId)
.orElseThrow(InvalidCredentialException::new);

if (!passwordEncoder.matches(oldPassword, user.getPasswordHash())) {
throw new InvalidCredentialException();
}
passwordPolicy.validate(newPassword);
if (passwordEncoder.matches(newPassword, user.getPasswordHash())) {
throw new PasswordReuseException();
}

user.setPasswordHash(passwordEncoder.encode(newPassword));
user.incrementCredentialVersion();
auditService.recordPasswordChanged(userId);
}

工具方法整理

HTTP 请求头

getHeaderValues 从请求头映射中读取指定字段。HTTP 头名称不区分大小写,工具方法应正确处理大小写与多值头,不能默认只有一个值。

分布式 ID

getWorkerID 为雪花算法提供工作节点标识。节点 ID 必须在部署范围内唯一,并处理时钟回拨问题。若配置错误,不同实例可能生成重复 ID。

摘要与文件签名

encodeMD5 不能用于密码存储,也不适合作为对抗恶意篡改的签名。若只用于非对抗场景的文件去重,应在命名和文档中明确用途。完整性保护应使用 SHA-256,身份认证场景应使用 HMAC 或数字签名。

1
2
3
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(fileBytes);
String checksum = HexFormat.of().formatHex(hash);

数字格式化

floatToStr 应明确舍入规则与区域设置。金额不应使用二进制浮点数,建议采用 BigDecimal

1
String display = amount.setScale(1, RoundingMode.HALF_UP).toPlainString();

PO 与 VO 转换

toVOtoPO 应只负责字段映射,不应隐式访问数据库或执行远程调用。列表转换要正确处理空集合,并对敏感字段设置显式忽略规则。

输入流转换

读取输入流时必须指定字符集、限制最大长度并正确关闭资源。对于大文件,应使用流式处理,避免一次性载入内存。

1
2
3
4
5
6
7
static String readUtf8(InputStream input, int maxBytes) throws IOException {
byte[] data = input.readNBytes(maxBytes + 1);
if (data.length > maxBytes) {
throw new IOException("input exceeds limit");
}
return new String(data, StandardCharsets.UTF_8);
}

IDEA 常用检索

功能 Windows 快捷键
全局动作搜索 连按两次 Shift
类搜索 Ctrl + N
文件搜索 Ctrl + Shift + N
符号搜索 Ctrl + Alt + Shift + N
全局文本搜索 Ctrl + Shift + F

故障排查顺序

  1. 浏览器确认请求 URL、状态码和响应体。
  2. OpenResty 日志确认静态命中或代理结果。
  3. Gateway 日志确认路由、鉴权和目标服务实例。
  4. 业务服务日志按请求 ID 追踪执行路径。
  5. 数据库与消息系统检查连接、事务和积压。
  6. 对照配置中心确认实际生效配置,而不是只查看本地文件。

原始系统架构图