Java全栈项目:校园共享单车管理平台
项目介绍
校园共享单车管理平台是一个基于Spring Boot + Vue.js的全栈项目,旨在为校园提供一个完整的共享单车管理解决方案。该系统支持学生租借单车、管理员管理车辆和用户等功能。
技术栈
后端
- Spring Boot 2.7.x
- Spring Security
- MyBatis-Plus
- MySQL 8.0
- Redis
- JWT
前端
- Vue 3
- Element Plus
- Axios
- Vuex
- Vue Router
核心功能模块
1. 用户管理模块
- 学生注册与登录
- 管理员账户管理
- 基于JWT的身份认证
- 角色权限控制
2. 单车管理模块
@Service
public class BikeService {
// 添加单车
public void addBike(BikeDTO bikeDTO) {
// 验证单车信息
// 生成唯一二维码
// 保存单车信息
}
// 查询可用单车
public List<BikeVO> getAvailableBikes() {
// 返回所有状态为可用的单车
}
}
3. 租借管理模块
- 扫码租车
- 实时计费
- 还车确认
- 故障报修
4. 支付模块
- 余额充值
- 费用计算
- 支付记录
数据库设计
主要包含以下表:
-- 用户表
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL,
balance DECIMAL(10,2) DEFAULT 0,
create_time DATETIME
);
-- 单车表
CREATE TABLE t_bike (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
bike_code VARCHAR(50) NOT NULL,
status INT DEFAULT 0,
location VARCHAR(100),
create_time DATETIME
);
-- 订单表
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
bike_id BIGINT,
start_time DATETIME,
end_time DATETIME,
amount DECIMAL(10,2),
status INT
);
项目亮点
- 采用微服务架构,便于扩展
- 使用Redis缓存热点数据
- 整合支付宝支付接口
- 实现实时位置追踪
- 基于AOP的操作日志记录
项目部署
- 使用Docker容器化部署
- Nginx反向代理
- Jenkins自动化部署
性能优化
- 数据库索引优化
- 接口缓存策略
- 前端资源压缩
- 图片懒加载
安全措施
- XSS防御
- SQL注入防护
- 敏感数据加密
- 接口限流
总结
本项目采用主流的Java全栈技术栈,实现了一个功能完整的校园共享单车管理系统。通过这个项目,不仅能够学习到完整的全栈开发流程,还能掌握项目部署和性能优化等实用技能。
校园共享单车管理平台 - 用户与单车模块详解
一、用户管理模块详细设计
1. 实体类设计
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String role; // ROLE_ADMIN, ROLE_STUDENT
private BigDecimal balance;
private String phone;
private String studentId; // 学号
private Integer status; // 0-禁用 1-正常
private LocalDateTime createTime;
}
2. 登录注册接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@PostMapping("/register")
public Result register(@RequestBody RegisterDTO dto) {
// 1. 验证学号是否已注册
// 2. 密码加密
// 3. 保存用户信息
return Result.success();
}
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
// 1. 验证用户名密码
// 2. 生成JWT Token
// 3. 返回用户信息和Token
return Result.success(loginVO);
}
}
3. JWT工具类
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("role", userDetails.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
// 验证token是否有效
}
}
4. 安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.antMatchers("/api/student/**").hasRole("STUDENT")
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
二、单车管理模块详细设计
1. 实体类设计
@Data
@TableName("t_bike")
public class Bike {
@TableId(type = IdType.AUTO)
private Long id;
private String bikeCode; // 单车编号
private Integer status; // 0-空闲 1-使用中 2-维修中 3-报废
private String location; // 位置信息
private Double latitude; // 纬度
private Double longitude; // 经度
private Integer battery; // 电量百分比
private LocalDateTime lastMaintenanceTime; // 最后维护时间
private LocalDateTime createTime;
}
2. 单车管理接口
@RestController
@RequestMapping("/api/bikes")
public class BikeController {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public Result addBike(@RequestBody BikeDTO dto) {
return bikeService.addBike(dto);
}
@GetMapping("/available")
public Result getAvailableBikes(
@RequestParam Double latitude,
@RequestParam Double longitude,
@RequestParam(defaultValue = "1000") Integer radius
) {
return bikeService.getNearbyBikes(latitude, longitude, radius);
}
@PutMapping("/{id}/status")
@PreAuthorize("hasRole('ADMIN')")
public Result updateBikeStatus(
@PathVariable Long id,
@RequestParam Integer status
) {
return bikeService.updateStatus(id, status);
}
}
3. 单车服务实现
@Service
@Slf4j
public class BikeServiceImpl implements BikeService {
@Autowired
private BikeMapper bikeMapper;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result addBike(BikeDTO dto) {
// 1. 生成唯一单车编号
String bikeCode = generateBikeCode();
// 2. 保存单车信息
Bike bike = new Bike();
BeanUtils.copyProperties(dto, bike);
bike.setBikeCode(bikeCode);
bike.setStatus(0);
bike.setCreateTime(LocalDateTime.now());
bikeMapper.insert(bike);
// 3. 更新Redis中的可用单车信息
String key = String.format("bike:location:%s", bike.getId());
redisTemplate.opsForGeo().add(
"bike:locations",
new Point(bike.getLongitude(), bike.getLatitude()),
key
);
return Result.success();
}
@Override
public Result getNearbyBikes(Double latitude, Double longitude, Integer radius) {
// 1. 从Redis中查询附近的单车
Circle circle = new Circle(
new Point(longitude, latitude),
new Distance(radius, Metrics.METERS)
);
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius("bike:locations", circle);
// 2. 转换为VO对象
List<BikeVO> bikes = convertToVOList(results);
return Result.success(bikes);
}
}
4. 缓存设计
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置序列化器
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
}
三、接口文档示例
用户注册
- 请求路径:
POST /api/auth/register
- 请求参数:
{
"username": "张三",
"password": "123456",
"studentId": "2021001",
"phone": "13800138000"
}
- 响应结果:
{
"code": 200,
"message": "注册成功",
"data": null
}
查询附近单车
- 请求路径:
GET /api/bikes/available
- 请求参数:
- latitude: 纬度
- longitude: 经度
- radius: 搜索半径(米)
- 响应结果:
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"bikeCode": "B001",
"distance": 100,
"battery": 90,
"latitude": 30.123456,
"longitude": 120.123456
}
]
}
四、注意事项
- 用户密码使用BCrypt加密存储
- JWT Token需要定期刷新
- 单车位置信息使用Redis GEO类型存储
- 接口调用需要做频率限制
- 重要操作需要记录审计日志
校园共享单车管理平台 - 租借与支付模块详解
一、租借管理模块
1. 订单实体设计
@Data
@TableName("t_rental_order")
public class RentalOrder {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long bikeId;
private String orderNo; // 订单编号
private Integer status; // 0-进行中 1-已完成 2-已取消
private BigDecimal amount; // 订单金额
private LocalDateTime startTime;
private LocalDateTime endTime;
private String startLocation; // 起始位置
private String endLocation; // 结束位置
private Integer duration; // 骑行时长(分钟)
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
2. 租车流程实现
@Service
@Slf4j
public class RentalServiceImpl implements RentalService {
@Autowired
private BikeService bikeService;
@Autowired
private RentalOrderMapper orderMapper;
@Transactional
@Override
public Result startRental(Long userId, String bikeCode) {
// 1. 验证用户状态和余额
UserInfo user = userService.checkUserStatus(userId);
// 2. 验证单车状态
Bike bike = bikeService.getBikeByCode(bikeCode);
if (bike.getStatus() != 0) {
throw new BusinessException("该单车不可租用");
}
// 3. 创建租借订单
RentalOrder order = new RentalOrder();
order.setUserId(userId);
order.setBikeId(bike.getId());
order.setOrderNo(generateOrderNo());
order.setStatus(0);
order.setStartTime(LocalDateTime.now());
order.setStartLocation(bike.getLocation());
orderMapper.insert(order);
// 4. 更新单车状态
bikeService.updateBikeStatus(bike.getId(), 1);
return Result.success(order);
}
@Transactional
@Override
public Result endRental(Long orderId, String endLocation) {
// 1. 获取订单信息
RentalOrder order = orderMapper.selectById(orderId);
if (order == null || order.getStatus() != 0) {
throw new BusinessException("无效的订单");
}
// 2. 计算费用
LocalDateTime now = LocalDateTime.now();
Duration duration = Duration.between(order.getStartTime(), now);
int minutes = (int) duration.toMinutes();
BigDecimal amount = calculateAmount(minutes);
// 3. 更新订单信息
order.setEndTime(now);
order.setEndLocation(endLocation);
order.setDuration(minutes);
order.setAmount(amount);
order.setStatus(1);
orderMapper.updateById(order);
// 4. 更新单车状态
bikeService.updateBikeStatus(order.getBikeId(), 0);
// 5. 扣除用户余额
userService.deductBalance(order.getUserId(), amount);
return Result.success(order);
}
}
3. 故障报修功能
@Data
@TableName("t_repair_record")
public class RepairRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long bikeId;
private Long userId;
private String description; // 故障描述
private String images; // 图片URL,多个用逗号分隔
private Integer status; // 0-待处理 1-处理中 2-已修复
private String handleNote; // 处理说明
private LocalDateTime createTime;
private LocalDateTime handleTime;
}
@RestController
@RequestMapping("/api/repairs")
public class RepairController {
@PostMapping
public Result reportRepair(@RequestBody RepairDTO dto) {
return repairService.createRepairRecord(dto);
}
@PutMapping("/{id}/handle")
@PreAuthorize("hasRole('ADMIN')")
public Result handleRepair(@PathVariable Long id, @RequestBody HandleDTO dto) {
return repairService.handleRepair(id, dto);
}
}
二、支付模块
1. 支付相关实体
@Data
@TableName("t_payment_record")
public class PaymentRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String tradeNo; // 交易流水号
private BigDecimal amount; // 充值金额
private Integer payType; // 1-支付宝 2-微信
private Integer status; // 0-待支付 1-支付成功 2-支付失败
private String paymentId; // 第三方支付ID
private LocalDateTime payTime; // 支付时间
private LocalDateTime createTime;
}
2. 余额充值实现
@Service
@Slf4j
public class PaymentServiceImpl implements PaymentService {
@Autowired
private AlipayClient alipayClient;
@Override
public Result createRecharge(RechargeDTO dto) {
// 1. 创建支付记录
PaymentRecord record = new PaymentRecord();
record.setUserId(dto.getUserId());
record.setAmount(dto.getAmount());
record.setPayType(dto.getPayType());
record.setTradeNo(generateTradeNo());
record.setStatus(0);
paymentRecordMapper.insert(record);
// 2. 调用支付宝接口
AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
request.setBizContent("{" +
"\"out_trade_no\":\"" + record.getTradeNo() + "\"," +
"\"total_amount\":\"" + record.getAmount() + "\"," +
"\"subject\":\"共享单车余额充值\"" +
"}");
try {
AlipayTradeCreateResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
return Result.success(response.getTradeNo());
}
} catch (AlipayApiException e) {
log.error("调用支付宝接口失败", e);
}
return Result.error("创建支付订单失败");
}
@Transactional
@Override
public void handlePayCallback(PayCallbackDTO dto) {
// 1. 验证支付信息
PaymentRecord record = paymentRecordMapper.selectByTradeNo(dto.getOutTradeNo());
if (record == null || record.getStatus() != 0) {
return;
}
// 2. 更新支付记录
record.setStatus(1);
record.setPaymentId(dto.getTradeNo());
record.setPayTime(LocalDateTime.now());
paymentRecordMapper.updateById(record);
// 3. 更新用户余额
userService.addBalance(record.getUserId(), record.getAmount());
}
}
3. 费用计算策略
@Service
public class BillingServiceImpl implements BillingService {
@Value("${billing.base-price}")
private BigDecimal basePrice; // 基础价格
@Value("${billing.per-minute}")
private BigDecimal perMinute; // 每分钟费用
@Override
public BigDecimal calculateAmount(int minutes) {
if (minutes <= 0) {
return BigDecimal.ZERO;
}
// 基础费用 + 时长费用
return basePrice.add(perMinute.multiply(new BigDecimal(minutes)));
}
@Override
public BigDecimal calculateDeposit() {
// 计算押金金额
return new BigDecimal("199");
}
}
4. 支付宝配置
@Configuration
public class AlipayConfig {
@Value("${alipay.app-id}")
private String appId;
@Value("${alipay.private-key}")
private String privateKey;
@Value("${alipay.public-key}")
private String publicKey;
@Bean
public AlipayClient alipayClient() {
return new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
appId,
privateKey,
"json",
"UTF-8",
publicKey,
"RSA2"
);
}
}
三、接口文档示例
开始租车
- 请求路径:
POST /api/rentals/start
- 请求参数:
{
"bikeCode": "B001"
}
- 响应结果:
{
"code": 200,
"message": "success",
"data": {
"orderNo": "RO202403150001",
"startTime": "2024-03-15 10:00:00"
}
}
创建充值订单
- 请求路径:
POST /api/payments/recharge
- 请求参数:
{
"amount": 50.00,
"payType": 1
}
- 响应结果:
{
"code": 200,
"message": "success",
"data": {
"tradeNo": "P202403150001",
"payUrl": "https://openapi.alipay.com/gateway.do?..."
}
}
四、注意事项
- 租车和还车操作需要添加分布式锁
- 支付回调要考虑重复通知的情况
- 费用计算需要考虑优惠券和会员折扣
- 重要金额操作需要记录详细日志
- 订单状态变更需要发送消息通知
- 支付接口需要做签名验证
- 图片上传需要做大小和格式限制
校园共享单车 - 微信支付接口集成
一、微信支付配置
1. 配置文件
wechat:
pay:
appId: wxxxxxxxxxxx
mchId: 1900000109
apiKey: 8934e7d15453e97507ef794cf7b0519d
apiV3Key: 31499L9VCD3M9G3j3JM9n8dj1f3j139
certPath: /path/to/apiclient_cert.p12
notifyUrl: https://example.com/api/payments/wx/callback
2. 微信支付配置类
@Configuration
@ConfigurationProperties(prefix = "wechat.pay")
@Data
public class WxPayConfig {
private String appId;
private String mchId;
private String apiKey;
private String apiV3Key;
private String certPath;
private String notifyUrl;
@Bean
public WxPayService wxPayService() throws Exception {
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(appId);
payConfig.setMchId(mchId);
payConfig.setMchKey(apiKey);
payConfig.setKeyPath(certPath);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
二、支付服务实现
1. 支付服务接口扩展
public interface PaymentService {
// 创建支付宝充值订单
Result createAlipayRecharge(RechargeDTO dto);
// 创建微信充值订单
Result createWxPayRecharge(RechargeDTO dto);
// 处理微信支付回调
void handleWxPayCallback(String xmlData);
}
2. 微信支付实现
@Service
@Slf4j
public class WxPayServiceImpl {
@Autowired
private WxPayService wxPayService;
@Autowired
private WxPayConfig wxPayConfig;
@Override
public Result createWxPayRecharge(RechargeDTO dto) {
try {
// 1. 创建支付记录
PaymentRecord record = createPaymentRecord(dto);
// 2. 构建支付请求参数
WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
request.setBody("共享单车余额充值");
request.setOutTradeNo(record.getTradeNo());
request.setTotalFee(convertToFen(dto.getAmount())); // 转换为分
request.setSpbillCreateIp(dto.getClientIp());
request.setNotifyUrl(wxPayConfig.getNotifyUrl());
request.setTradeType("JSAPI"); // 小程序支付
request.setOpenid(dto.getOpenid());
// 3. 调用统一下单接口
WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request);
// 4. 生成支付参数
WxPayMpOrderResult payInfo = WxPayMpOrderResult.builder()
.appId(result.getAppid())
.timeStamp(String.valueOf(System.currentTimeMillis() / 1000))
.nonceStr(result.getNonceStr())
.packageValue("prepay_id=" + result.getPrepayId())
.signType("MD5")
.build();
// 5. 签名
payInfo.setPaySign(SignUtils.createSign(payInfo, wxPayConfig.getApiKey()));
return Result.success(payInfo);
} catch (WxPayException e) {
log.error("微信支付下单失败", e);
return Result.error("创建支付订单失败");
}
}
@Transactional
@Override
public void handleWxPayCallback(String xmlData) {
try {
// 1. 解析回调数据
WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(xmlData);
// 2. 验证支付结果
if (!"SUCCESS".equals(notifyResult.getResultCode())) {
log.error("支付失败:{}", notifyResult.getErrCodeDes());
return;
}
// 3. 查询支付记录
PaymentRecord record = paymentRecordMapper.selectByTradeNo(
notifyResult.getOutTradeNo()
);
if (record == null || record.getStatus() != 0) {
return;
}
// 4. 验证支付金额
if (!record.getAmount().multiply(new BigDecimal("100"))
.equals(new BigDecimal(notifyResult.getTotalFee()))) {
log.error("支付金额不匹配");
return;
}
// 5. 更新支付记录
record.setStatus(1);
record.setPaymentId(notifyResult.getTransactionId());
record.setPayTime(LocalDateTime.now());
paymentRecordMapper.updateById(record);
// 6. 更新用户余额
userService.addBalance(record.getUserId(), record.getAmount());
} catch (WxPayException e) {
log.error("处理微信支付回调失败", e);
throw new BusinessException("处理支付回调失败");
}
}
private Integer convertToFen(BigDecimal amount) {
return amount.multiply(new BigDecimal("100")).intValue();
}
}
3. 支付回调接口
@RestController
@RequestMapping("/api/payments")
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping("/wx/callback")
public String handleWxPayCallback(HttpServletRequest request) {
try {
// 1. 读取回调数据
String xmlData = IOUtils.toString(request.getInputStream(), "UTF-8");
log.info("收到微信支付回调:{}", xmlData);
// 2. 处理回调
paymentService.handleWxPayCallback(xmlData);
// 3. 返回成功响应
return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
} catch (Exception e) {
log.error("处理微信支付回调异常", e);
return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理失败]]></return_msg></xml>";
}
}
}
三、工具类
1. 签名工具类
public class SignUtils {
public static String createSign(Object data, String key) {
// 1. 将对象转为Map
Map<String, String> params = objectToMap(data);
// 2. 按照键值排序
TreeMap<String, String> sortedParams = new TreeMap<>(params);
// 3. 拼接签名字符串
StringBuilder signStr = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
if (StringUtils.isNotEmpty(entry.getValue())) {
signStr.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
}
signStr.append("key=").append(key);
// 4. MD5加密
return DigestUtils.md5Hex(signStr.toString()).toUpperCase();
}
private static Map<String, String> objectToMap(Object obj) {
// 使用反射将对象转换为Map
}
}
四、接口文档
创建微信支付订单
- 请求路径:
POST /api/payments/wx/recharge
- 请求参数:
{
"amount": 50.00,
"openid": "oxxxxxxxxxxxxxxxxxx",
"clientIp": "127.0.0.1"
}
- 响应结果:
{
"code": 200,
"message": "success",
"data": {
"appId": "wxxxxxxxxxxx",
"timeStamp": "1634567890",
"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
"packageValue": "prepay_id=wx201410272009395522657a690389285100",
"signType": "MD5",
"paySign": "C380BEC2BFD727A4B6845133519F3AD6"
}
}
五、注意事项
- 安全考虑
@Configuration
public class WxPaySecurityConfig {
@Bean
public SignatureVerifier wxPaySignatureVerifier() {
return new SignatureVerifier() {
@Override
public boolean verify(String body, String signature) {
// 验证签名
}
};
}
@Bean
public RequestInterceptor wxPayRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void intercept(RequestFacade request) {
// 添加请求头
request.addHeader("Content-Type", "application/json");
request.addHeader("Accept", "application/json");
}
};
}
}
-
关键注意点:
- 所有金额计算使用BigDecimal
- 微信支付金额单位为分
- 支付回调需要验证签名
- 回调接口需要做幂等处理
- 敏感配置信息需要加密存储
- 证书文件需要安全保存
- 重要操作需要记录日志
- 考虑并发情况下的数据一致性
-
异常处理:
@ControllerAdvice
public class WxPayExceptionHandler {
@ExceptionHandler(WxPayException.class)
public Result handleWxPayException(WxPayException e) {
log.error("微信支付异常", e);
return Result.error("支付处理失败");
}
}
- 支付状态轮询:
@Scheduled(fixedDelay = 60000)
public void checkPaymentStatus() {
List<PaymentRecord> pendingRecords = paymentRecordMapper.selectByStatus(0);
for (PaymentRecord record : pendingRecords) {
try {
WxPayOrderQueryResult result = wxPayService.queryOrder("", record.getTradeNo());
if ("SUCCESS".equals(result.getTradeState())) {
handleWxPayCallback(result);
}
} catch (WxPayException e) {
log.error("查询支付状态失败", e);
}
}
}