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. 先构建 commons 与 modules,再构建依赖它们的服务。
  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 转换

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

输入流转换

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

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. 对照配置中心确认实际生效配置,而不是只查看本地文件。

原始系统架构图