汇丰项目开发实战:从需求到上线
一个典型需求的完整开发流程
场景示例
假设你收到了这样一个需求:
JIRA-1234: 作为储户,我希望查看我的交易明细,这样我可以核对每笔支出
第一步:需求评审
参与角色
- Product Owner (业务方)
- Tech Lead (技术负责人)
- BA (业务分析师)
- QA (测试工程师)
- 开发者 (You!)
评审要点
业务规则
- 显示多长时间的交易记录?(最近3个月/6个月/1年)
- 如何排序?(按时间倒序/正序)
- 每页显示多少条?
边界情况
- 没有交易记录怎么显示?
- 数据加载超时怎么办?
- 网络断开怎么处理?
合规要求
- 交易金额是否需要脱敏?
- 账户号是否需要掩码?
需求澄清Checklist
markdown
## 需求澄清记录
### 基础信息
- JIRA编号: JIRA-1234
- 优先级: P0/P1/P2/P3
- 预估工作量: X 人天
- Sprint: Y
### 业务规则
- [ ] 规则1: ...
- [ ] 规则2: ...
### 异常场景
- [ ] 场景1: ...
- [ ] 场景2: ...
### 合规检查
- [ ] 需要数据脱敏?
- [ ] 需要日志脱敏?
- [ ] 需要审计日志?
### 依赖方
- [ ] 依赖系统A
- [ ] 依赖系统B第二步:技术设计
设计文档模板
markdown
# JIRA-1234 交易明细查询 - 技术设计
## 1. 需求理解
[简述需求]
## 2. 技术方案
- 后端:新增 `/api/v1/accounts/{accountId}/transactions` 接口
- 前端:新增交易明细页面
- 数据库:使用现有 transactions 表
## 3. 接口设计
### Request
GET /api/v1/accounts/{accountId}/transactions?page=0&size=20&sort=date,desc
### Response
{
"content": [
{
"transactionId": "TX123456",
"date": "2026-02-26T10:30:00Z",
"amount": -100.00,
"type": "DEBIT",
"description": "超市消费",
"merchant": "沃尔玛"
}
],
"totalElements": 150,
"totalPages": 8
}
## 4. 数据库设计
[如有新表或字段变更]
## 5. 风险评估
- 性能:大数据量需加索引
- 安全:返回数据需脱敏
## 6. 验收标准
- [ ] 支持分页查询
- [ ] 支持按日期排序
- [ ] 单元测试覆盖率 > 70%
- [ ] 通过UAT测试第三步:代码开发
后端实现
java
// TransactionController.java
@RestController
@RequestMapping("/api/v1/accounts")
@RequiredArgsConstructor
@Slf4j
public class TransactionController {
private final TransactionService transactionService;
@GetMapping("/{accountId}/transactions")
public ResponseEntity<Page<TransactionDTO>> getTransactions(
@PathVariable String accountId,
@PageableDefault(size = 20, sort = "transactionDate") Pageable pageable,
@RequestHeader("X-User-ID") String userId) {
// 1. 验证账户属于当前用户
accountService.validateOwnership(accountId, userId);
// 2. 查询交易记录
Page<TransactionDTO> transactions =
transactionService.findByAccountId(accountId, pageable);
// 3. 记录审计日志
auditService.log(userId, "VIEW_TRANSACTIONS", accountId);
return ResponseEntity.ok(transactions);
}
}java
// TransactionService.java
@Service
@RequiredArgsConstructor
public class TransactionService {
private final TransactionRepository transactionRepository;
public Page<TransactionDTO> findByAccountId(String accountId, Pageable pageable) {
return transactionRepository
.findByAccountIdOrderByTransactionDateDesc(accountId, pageable)
.map(this::toDTO);
}
private TransactionDTO toDTO(Transaction transaction) {
return TransactionDTO.builder()
.transactionId(transaction.getId())
.date(transaction.getTransactionDate())
// 金额保留,但需要脱敏处理
.amount(transaction.getAmount())
.type(transaction.getType())
.description(maskDescription(transaction.getDescription()))
.build();
}
private String maskDescription(String description) {
// 如果是敏感商户,进行脱敏
if (sensitiveMerchantService.isSensitive(description)) {
return "商户交易";
}
return description;
}
}前端实现
tsx
// TransactionList.tsx
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Table, DatePicker, Spin, message } from 'antd';
import { transactionApi } from '@/api';
const { RangePicker } = DatePicker;
export const TransactionList: React.FC = () => {
const { accountId } = useParams<{ accountId: string }>();
const [loading, setLoading] = useState(false);
const [transactions, setTransactions] = useState([]);
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
const fetchTransactions = async (page: number) => {
setLoading(true);
try {
const response = await transactionApi.getTransactions(accountId!, {
page: page - 1,
size: pagination.pageSize,
sort: 'transactionDate,desc'
});
setTransactions(response.content);
setPagination({
...pagination,
current: page,
total: response.totalElements
});
} catch (error) {
message.error('加载交易记录失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTransactions(1);
}, [accountId]);
const columns = [
{
title: '交易时间',
dataIndex: 'date',
key: 'date',
render: (date: string) => new Date(date).toLocaleString('zh-CN')
},
{
title: '交易金额',
dataIndex: 'amount',
key: 'amount',
render: (amount: number, record: any) => (
<span style={{ color: amount < 0 ? '#ff4d4f' : '#52c41a' }}>
{amount < 0 ? '-' : '+'}{Math.abs(amount).toFixed(2)}
</span>
)
},
{
title: '交易类型',
dataIndex: 'type',
key: 'type'
},
{
title: '描述',
dataIndex: 'description',
key: 'description'
}
];
return (
<Spin spinning={loading}>
<Table
columns={columns}
dataSource={transactions}
rowKey="transactionId"
pagination={{
...pagination,
onChange: fetchTransactions
}}
/>
</Spin>
);
};第四步:Code Review
常见Review问题
java
// ❌ 问题1: 没有参数校验
public ResponseEntity getTransactions(String accountId) {
// 直接使用 accountId,没有验证
return transactionService.findByAccountId(accountId);
}
// ✅ 修正
public ResponseEntity getTransactions(
@PathVariable @NotBlank String accountId,
@RequestHeader @NotBlank String X-User-ID) {
// 参数校验 + 归属验证
accountService.validateOwnership(accountId, X-User-ID);
}java
// ❌ 问题2: 异常被吞掉
try {
return transactionService.findByAccountId(accountId);
} catch (Exception e) {
// 不要这样做!
return null;
}
// ✅ 修正
try {
return transactionService.findByAccountId(accountId);
} catch (Exception e) {
log.error("查询交易失败, accountId={}, error={}", accountId, e.getMessage());
throw new TransactionQueryException("系统错误,请稍后重试");
}java
// ❌ 问题3: 关键操作没有日志
public void processPayment(PaymentRequest request) {
paymentGateway.process(request);
// 没有记录日志,出问题无法追溯
}
// ✅ 修正
public void processPayment(PaymentRequest request) {
log.info("开始处理支付, requestId={}, amount={}",
request.getRequestId(), request.getAmount());
try {
paymentGateway.process(request);
log.info("支付处理成功, requestId={}", request.getRequestId());
} catch (Exception e) {
log.error("支付处理失败, requestId={}, error={}",
request.getRequestId(), e.getMessage());
throw e;
}
}第五步:测试
单元测试要求
java
@ExtendWith(MockitoExtension.class)
class TransactionServiceTest {
@Mock
private TransactionRepository transactionRepository;
@InjectMocks
private TransactionService transactionService;
@Test
void shouldReturnTransactionsWhenAccountExists() {
// Arrange
String accountId = "ACC123";
Pageable pageable = PageRequest.of(0, 20);
Transaction transaction = Transaction.builder()
.id("TX001")
.accountId(accountId)
.amount(new BigDecimal("100.00"))
.transactionDate(LocalDateTime.now())
.build();
when(transactionRepository.findByAccountIdOrderByTransactionDateDesc(
eq(accountId), any(Pageable.class)))
.thenReturn(new PageImpl<>(List.of(transaction)));
// Act
Page<TransactionDTO> result = transactionService.findByAccountId(accountId, pageable);
// Assert
assertEquals(1, result.getTotalElements());
assertEquals("TX001", result.getContent().get(0).getTransactionId());
}
@Test
void shouldReturnEmptyWhenNoTransactions() {
// Arrange
String accountId = "ACC999";
Pageable pageable = PageRequest.of(0, 20);
when(transactionRepository.findByAccountIdOrderByTransactionDateDesc(
eq(accountId), any(Pageable.class)))
.thenReturn(new PageImpl<>(List.of()));
// Act
Page<TransactionDTO> result = transactionService.findByAccountId(accountId, pageable);
// Assert
assertEquals(0, result.getTotalElements());
assertTrue(result.getContent().isEmpty());
}
}测试覆盖率要求
| 模块 | 最低覆盖率 |
|---|---|
| Service层 | 80% |
| Controller层 | 60% |
| 整体项目 | 70% |
第六步:UAT测试
UAT要点
- 正常流程:用户能正常查看交易明细
- 边界情况:
- 空账户(无交易)
- 大数据量(1000+条)
- 分页正常
- 异常情况:
- 网络超时
- 服务不可用
- 合规检查:
- 数据脱敏正确
- 日志无敏感信息
第七步:上线
上线Checklist
bash
# 1. 确保所有测试通过
./mvnw test
# 2. 构建生产包
./mvnw clean package -Pproduction
# 3. 打包Docker镜像
docker build -t hsbc-transaction-service:v1.2.3 .
# 4. 推送到仓库
docker push registry.hsbc.com/hsbc-transaction-service:v1.2.3
# 5. 更新K8s部署
kubectl set image deployment/transaction-service \
transaction-service=registry.hsbc.com/hsbc-transaction-service:v1.2.3
# 6. 检查上线状态
kubectl rollout status deployment/transaction-service上线后验证
- 检查新接口是否正常响应
- 监控错误率和响应时间
- 确认业务方验收
附加:CI/CD Pipeline 详解
典型的Jenkins/GitLab CI流程
yaml
# .gitlab-ci.yml 示例
stages:
- build
- test
- security-scan
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
build:
stage: build
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
test:
stage: test
script:
- mvn test
- mvn jacoco:report
coverage: '/Total.*?([0-9]{1,3})%/'
security-scan:
stage: security-scan
script:
- mvn dependency:tree
- mvn snyk:test # 安全扫描
deploy-staging:
stage: deploy
script:
- kubectl apply -f k8s/staging/
only:
- develop
deploy-production:
stage: deploy
script:
- kubectl apply -f k8s/production/
when: manual # 手动触发
only:
- main质量门禁
| 检查项 | 阈值 | 工具 |
|---|---|---|
| 单元测试覆盖率 | > 70% | JaCoCo |
| 代码风格 | 0 警告 | SonarQube |
| 安全漏洞 | 0 高危 | Snyk/OWASP |
| 构建状态 | 成功 | CI系统 |
附加:上线回滚流程
回滚触发条件
- 错误率 > 1%
- 响应时间 P99 > 2s
- 业务功能异常
- 严重Bug
回滚操作步骤
bash
# 1. 查看当前版本
kubectl get deployment transaction-service -n production
# 2. 执行回滚
kubectl rollout undo deployment/transaction-service -n production
# 3. 验证回滚
kubectl rollout status deployment/transaction-service -n production
# 4. 检查旧版本是否正常运行
kubectl get pods -n production -l app=transaction-service
# 5. 通知相关团队
# - 邮件通知
# - Slack/Teams 群通知回滚后行动
- 保留现场:不要删除有问题的Pod/镜像
- 收集证据:日志、监控截图、错误堆栈
- 立即上报:告知Tech Lead和PO
- 复盘会议:48小时内完成问题复盘
附加:日常开发 Checklist
markdown
## 代码提交前检查
- [ ] 本地测试全部通过
- [ ] 代码格式符合规范 (Spotless/Prettier)
- [ ] 没有打印敏感信息 (密码/Token)
- [ ] 注释和文档已更新
- [ ] 单元测试已添加
- [ ] Commit信息符合规范
## Code Review自检
- [ ] 代码逻辑正确
- [ ] 异常处理完善
- [ ] 日志记录适当
- [ ] 没有硬编码
- [ ] 变量命名清晰
- [ ] 方法长度合理 (< 50行)总结
这就是一个典型银行需求的完整开发流程。关键点:
- 📝 文档完整:设计文档、注释要清晰
- 🛡️ 安全第一:数据脱敏、日志规范
- ✅ 测试充分:单元测试、UAT
- 📊 监控到位:上线后持续监控
- 🔄 回滚预案:随时准备回退
祝你开发顺利!🚀