Skip to content

汇丰项目开发实战:从需求到上线

一个典型需求的完整开发流程


场景示例

假设你收到了这样一个需求:

JIRA-1234: 作为储户,我希望查看我的交易明细,这样我可以核对每笔支出


第一步:需求评审

参与角色

  • Product Owner (业务方)
  • Tech Lead (技术负责人)
  • BA (业务分析师)
  • QA (测试工程师)
  • 开发者 (You!)

评审要点

  1. 业务规则

    • 显示多长时间的交易记录?(最近3个月/6个月/1年)
    • 如何排序?(按时间倒序/正序)
    • 每页显示多少条?
  2. 边界情况

    • 没有交易记录怎么显示?
    • 数据加载超时怎么办?
    • 网络断开怎么处理?
  3. 合规要求

    • 交易金额是否需要脱敏?
    • 账户号是否需要掩码?

需求澄清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要点

  1. 正常流程:用户能正常查看交易明细
  2. 边界情况
    • 空账户(无交易)
    • 大数据量(1000+条)
    • 分页正常
  3. 异常情况
    • 网络超时
    • 服务不可用
  4. 合规检查
    • 数据脱敏正确
    • 日志无敏感信息

第七步:上线

上线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

上线后验证

  1. 检查新接口是否正常响应
  2. 监控错误率和响应时间
  3. 确认业务方验收

附加: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 群通知

回滚后行动

  1. 保留现场:不要删除有问题的Pod/镜像
  2. 收集证据:日志、监控截图、错误堆栈
  3. 立即上报:告知Tech Lead和PO
  4. 复盘会议:48小时内完成问题复盘

附加:日常开发 Checklist

markdown
## 代码提交前检查

- [ ] 本地测试全部通过
- [ ] 代码格式符合规范 (Spotless/Prettier)
- [ ] 没有打印敏感信息 (密码/Token)
- [ ] 注释和文档已更新
- [ ] 单元测试已添加
- [ ] Commit信息符合规范

## Code Review自检

- [ ] 代码逻辑正确
- [ ] 异常处理完善
- [ ] 日志记录适当
- [ ] 没有硬编码
- [ ] 变量命名清晰
- [ ] 方法长度合理 (< 50行)

总结

这就是一个典型银行需求的完整开发流程。关键点:

  • 📝 文档完整:设计文档、注释要清晰
  • 🛡️ 安全第一:数据脱敏、日志规范
  • 测试充分:单元测试、UAT
  • 📊 监控到位:上线后持续监控
  • 🔄 回滚预案:随时准备回退

祝你开发顺利!🚀

> 学而时习之,不亦说乎?