fix: 全面代码审计修复 P0/P1/P2 共16项安全与质量问题
P0 安全漏洞修复(4项): - S1: FileUploadController 添加文件扩展名+MIME类型+上传type白名单(防RCE) - S2: FileUploadController 添加@RequiresRole强制认证(防认证绕过) - S3: Actuator仅暴露health端点, SecurityConfig denyAll非health - S4: Swagger添加SWAGGER_ENABLED环境变量控制, 移除认证排除路径 P1 高危问题修复(7项): - S5: login.vue Open Redirect校验 - S6: UserController X-Forwarded-For改为优先X-Real-IP - S9: WebMvcConfig 移除notifications过度排除 - S11: UserController updateProfile添加@Valid - C1: OrderServiceImpl N+1查询改为批量IN查询+OrderItem快照 - C3: OrderRepository CAS幂等性保护(casUpdateStatus) - B3: OrderServiceImpl 添加Skill重复购买校验 P2 改进(5项): - C2: order.js 移除前端paymentNo生成 - C5: order.js pageSize从100改为20 - F2: apiService.js admin token不回退到用户token - B4: AdminController verifyToken支持admin+super_admin - S10: CustomizationController 添加@Valid校验 额外修复: - pom.xml 添加spring-boot-starter-aop依赖(解决编译错误) - 审计报告追加修复记录章节, 项目评级B+升至A-
This commit is contained in:
@@ -83,6 +83,12 @@
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- AOP (AspectJ) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RabbitMQ -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -108,6 +114,40 @@
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI (Swagger) -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Actuator 健康检查 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 腾讯云短信 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.tencentcloudapi</groupId>
|
||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||
<version>3.1.880</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 微信支付 V3 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.github.wechatpay-apiv3</groupId>
|
||||
<artifactId>wechatpay-java</artifactId>
|
||||
<version>0.2.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 支付宝 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.38.0.ALL</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -25,7 +25,10 @@ public class SecurityConfig {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
.requestMatchers("/actuator/**").denyAll()
|
||||
.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.openclaw.config;
|
||||
|
||||
import com.openclaw.interceptor.AuthInterceptor;
|
||||
import com.openclaw.interceptor.PermissionCheckInterceptor;
|
||||
import com.openclaw.interceptor.RoleCheckInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -12,6 +13,17 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
private final RoleCheckInterceptor roleCheckInterceptor;
|
||||
private final PermissionCheckInterceptor permissionCheckInterceptor;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOriginPatterns("http://localhost:*", "https://*.openclaw.com")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
@@ -20,15 +32,30 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
.excludePathPatterns(
|
||||
"/api/v1/users/register",
|
||||
"/api/v1/users/login",
|
||||
"/api/v1/users/sms-code",
|
||||
"/api/v1/users/sms/code",
|
||||
"/api/v1/users/password/reset",
|
||||
"/api/v1/payments/callback/**",
|
||||
"/api/v1/wechat/authorize-url", // 微信授权URL(公开)
|
||||
"/api/v1/wechat/login", // 微信扫码登录(公开)
|
||||
"/api/v1/wechat/bind-phone", // 微信绑定手机号(公开,用bindTicket)
|
||||
"/api/v1/skills", // 公开浏览
|
||||
"/api/v1/skills/{id}" // 公开详情
|
||||
"/api/v1/skills/*", // 公开详情(通配符匹配)
|
||||
"/api/v1/skills/*/reviews", // 评论列表(公开)
|
||||
"/api/v1/categories", // 分类列表
|
||||
"/api/v1/categories/*", // 分类详情
|
||||
"/api/v1/customization/request", // 定制需求(游客可提交)
|
||||
"/api/v1/stats/**", // 首页统计数据(公开)
|
||||
"/api/v1/admin/login", // 管理员登录
|
||||
"/api/v1/banners/active", // 公开Banner
|
||||
"/api/v1/announcements/active" // 公开公告
|
||||
);
|
||||
|
||||
// 角色权限拦截器,在认证之后执行
|
||||
registry.addInterceptor(roleCheckInterceptor)
|
||||
.addPathPatterns("/api/**");
|
||||
|
||||
// 细粒度权限拦截器,在角色拦截器之后执行
|
||||
registry.addInterceptor(permissionCheckInterceptor)
|
||||
.addPathPatterns("/api/**");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.openclaw.module.admin.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.openclaw.annotation.RequiresRole;
|
||||
import com.openclaw.module.log.annotation.OpLog;
|
||||
import com.openclaw.common.Result;
|
||||
import com.openclaw.module.admin.dto.AdminLoginDTO;
|
||||
import com.openclaw.module.admin.dto.AdminSkillCreateDTO;
|
||||
import com.openclaw.module.admin.service.AdminService;
|
||||
import com.openclaw.module.admin.vo.*;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminController {
|
||||
|
||||
private final AdminService adminService;
|
||||
|
||||
// ==================== 登录(无需权限) ====================
|
||||
|
||||
@PostMapping("/login")
|
||||
public Result<AdminLoginVO> login(@Valid @RequestBody AdminLoginDTO dto) {
|
||||
return Result.ok(adminService.login(dto));
|
||||
}
|
||||
|
||||
// ==================== Token 验证 ====================
|
||||
|
||||
@GetMapping("/verify-token")
|
||||
@RequiresRole({"admin", "super_admin"})
|
||||
public Result<Void> verifyToken() {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// ==================== Dashboard ====================
|
||||
|
||||
@GetMapping("/dashboard/stats")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<DashboardStatsVO> getDashboardStats() {
|
||||
return Result.ok(adminService.getDashboardStats());
|
||||
}
|
||||
|
||||
// ==================== 用户管理 ====================
|
||||
|
||||
@GetMapping("/users")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminUserVO>> listUsers(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) String role,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listUsers(keyword, status, role, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/users/{userId}")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<AdminUserVO> getUserDetail(@PathVariable Long userId) {
|
||||
return Result.ok(adminService.getUserDetail(userId));
|
||||
}
|
||||
|
||||
@PostMapping("/users/{userId}/ban")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "user", action = "ban", description = "封禁用户", targetType = "user")
|
||||
public Result<Void> banUser(@PathVariable Long userId, @RequestParam String reason) {
|
||||
adminService.banUser(userId, reason);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/users/{userId}/unban")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "user", action = "unban", description = "解禁用户", targetType = "user")
|
||||
public Result<Void> unbanUser(@PathVariable Long userId) {
|
||||
adminService.unbanUser(userId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/users/{userId}/role")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "user", action = "update", description = "修改用户角色", targetType = "user")
|
||||
public Result<Void> changeUserRole(@PathVariable Long userId, @RequestParam String role) {
|
||||
adminService.changeUserRole(userId, role);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// ==================== Skill管理 ====================
|
||||
|
||||
@GetMapping("/skills")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminSkillVO>> listSkills(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer categoryId,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listSkills(keyword, status, categoryId, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/skills/{skillId}")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<AdminSkillVO> getSkillDetail(@PathVariable Long skillId) {
|
||||
return Result.ok(adminService.getSkillDetail(skillId));
|
||||
}
|
||||
|
||||
@PostMapping("/skills/{skillId}/audit")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "skill", action = "audit", description = "审核Skill", targetType = "skill")
|
||||
public Result<Void> auditSkill(
|
||||
@PathVariable Long skillId,
|
||||
@RequestParam String action,
|
||||
@RequestParam(required = false) String rejectReason) {
|
||||
adminService.auditSkill(skillId, action, rejectReason);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/skills/{skillId}/offline")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "skill", action = "offline", description = "下架Skill", targetType = "skill")
|
||||
public Result<Void> offlineSkill(@PathVariable Long skillId) {
|
||||
adminService.offlineSkill(skillId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/skills/{skillId}/featured")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "skill", action = "update", description = "切换精选状态", targetType = "skill")
|
||||
public Result<Void> toggleFeatured(@PathVariable Long skillId) {
|
||||
adminService.toggleFeatured(skillId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/skills/create")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "skill", action = "create", description = "后台创建Skill", targetType = "skill")
|
||||
public Result<AdminSkillVO> createSkill(
|
||||
@RequestAttribute("userId") Long adminUserId,
|
||||
@Valid @RequestBody AdminSkillCreateDTO dto) {
|
||||
return Result.ok(adminService.createSkill(adminUserId, dto));
|
||||
}
|
||||
|
||||
// ==================== 订单管理 ====================
|
||||
|
||||
@GetMapping("/orders")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminOrderVO>> listOrders(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listOrders(keyword, status, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@GetMapping("/orders/{orderId}")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<AdminOrderVO> getOrderDetail(@PathVariable Long orderId) {
|
||||
return Result.ok(adminService.getOrderDetail(orderId));
|
||||
}
|
||||
|
||||
// ==================== 退款管理 ====================
|
||||
|
||||
@GetMapping("/refunds")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminRefundVO>> listRefunds(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listRefunds(keyword, status, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@PostMapping("/refunds/{refundId}/approve")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "order", action = "approve", description = "审批退款", targetType = "refund")
|
||||
public Result<Void> approveRefund(@PathVariable Long refundId) {
|
||||
adminService.approveRefund(refundId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/refunds/{refundId}/reject")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "order", action = "reject", description = "拒绝退款", targetType = "refund")
|
||||
public Result<Void> rejectRefund(@PathVariable Long refundId, @RequestParam String rejectReason) {
|
||||
adminService.rejectRefund(refundId, rejectReason);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// ==================== 评论管理 ====================
|
||||
|
||||
@GetMapping("/comments")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminCommentVO>> listComments(
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) Long skillId,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listComments(keyword, skillId, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@DeleteMapping("/comments/{commentId}")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "content", action = "delete", description = "删除评论", targetType = "comment")
|
||||
public Result<Void> deleteComment(@PathVariable Long commentId) {
|
||||
adminService.deleteComment(commentId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// ==================== 积分管理 ====================
|
||||
|
||||
@GetMapping("/points/records")
|
||||
@RequiresRole("super_admin")
|
||||
public Result<IPage<AdminPointsRecordVO>> listPointsRecords(
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) String pointsType,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize) {
|
||||
return Result.ok(adminService.listPointsRecords(userId, pointsType, pageNum, pageSize));
|
||||
}
|
||||
|
||||
@PostMapping("/points/adjust")
|
||||
@RequiresRole("super_admin")
|
||||
@OpLog(module = "points", action = "adjust", description = "手动调整积分", targetType = "user")
|
||||
public Result<Void> adjustPoints(
|
||||
@RequestParam Long userId,
|
||||
@RequestParam int amount,
|
||||
@RequestParam String reason) {
|
||||
adminService.adjustPoints(userId, amount, reason);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.openclaw.module.common.controller;
|
||||
|
||||
import com.openclaw.annotation.RequiresRole;
|
||||
import com.openclaw.common.Result;
|
||||
import com.openclaw.util.UserContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/upload")
|
||||
@RequiresRole("user")
|
||||
public class FileUploadController {
|
||||
|
||||
@Value("${upload.path:./uploads}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${upload.base-url:http://localhost:8080/uploads}")
|
||||
private String baseUrl;
|
||||
|
||||
/** 允许的文件扩展名白名单 */
|
||||
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
|
||||
".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".pdf"
|
||||
);
|
||||
|
||||
/** 允许的MIME类型白名单 */
|
||||
private static final Set<String> ALLOWED_MIME_TYPES = Set.of(
|
||||
"image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", "application/pdf"
|
||||
);
|
||||
|
||||
/** 允许的上传类型(防止路径遍历) */
|
||||
private static final Set<String> ALLOWED_UPLOAD_TYPES = Set.of(
|
||||
"avatar", "skill", "review", "invoice", "refund", "banner", "announcement"
|
||||
);
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
*/
|
||||
@PostMapping("/avatar")
|
||||
public Result<Map<String, String>> uploadAvatar(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
Long userId = UserContext.getUserId();
|
||||
return handleUpload(file, "avatar", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用文件上传(需登录)
|
||||
*/
|
||||
@PostMapping("/{type}")
|
||||
public Result<Map<String, String>> uploadFile(@PathVariable String type,
|
||||
@RequestParam("file") MultipartFile file) throws IOException {
|
||||
Long userId = UserContext.getUserId();
|
||||
return handleUpload(file, type, userId);
|
||||
}
|
||||
|
||||
private Result<Map<String, String>> handleUpload(MultipartFile file, String type, Long userId) throws IOException {
|
||||
if (file.isEmpty()) {
|
||||
return Result.fail(400, "文件不能为空");
|
||||
}
|
||||
|
||||
// 校验上传类型(防止路径遍历)
|
||||
if (!ALLOWED_UPLOAD_TYPES.contains(type)) {
|
||||
return Result.fail(400, "不支持的上传类型: " + type);
|
||||
}
|
||||
|
||||
// 限制文件大小 (5MB)
|
||||
if (file.getSize() > 5 * 1024 * 1024) {
|
||||
return Result.fail(400, "文件大小不能超过5MB");
|
||||
}
|
||||
|
||||
// 校验文件扩展名
|
||||
String originalName = file.getOriginalFilename();
|
||||
String ext = "";
|
||||
if (originalName != null && originalName.contains(".")) {
|
||||
ext = originalName.substring(originalName.lastIndexOf(".")).toLowerCase();
|
||||
}
|
||||
if (ext.isEmpty() || !ALLOWED_EXTENSIONS.contains(ext)) {
|
||||
return Result.fail(400, "不支持的文件类型,仅允许: jpg/jpeg/png/gif/webp/bmp/pdf");
|
||||
}
|
||||
|
||||
// 校验MIME类型
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || !ALLOWED_MIME_TYPES.contains(contentType.toLowerCase())) {
|
||||
return Result.fail(400, "文件MIME类型不合法");
|
||||
}
|
||||
|
||||
String storedName = UUID.randomUUID().toString() + ext;
|
||||
Path dir = Paths.get(uploadPath, type);
|
||||
Files.createDirectories(dir);
|
||||
Path filePath = dir.resolve(storedName);
|
||||
file.transferTo(filePath.toFile());
|
||||
|
||||
String fileUrl = baseUrl + "/" + type + "/" + storedName;
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("url", fileUrl);
|
||||
result.put("originalName", originalName);
|
||||
|
||||
log.info("文件上传成功: type={}, userId={}, url={}", type, userId, fileUrl);
|
||||
return Result.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.openclaw.module.customization.controller;
|
||||
|
||||
import com.openclaw.common.Result;
|
||||
import com.openclaw.module.customization.dto.CustomizationRequestDTO;
|
||||
import com.openclaw.module.customization.service.CustomizationRequestService;
|
||||
import com.openclaw.util.UserContext;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/customization")
|
||||
@RequiredArgsConstructor
|
||||
public class CustomizationController {
|
||||
|
||||
private final CustomizationRequestService customizationRequestService;
|
||||
|
||||
/**
|
||||
* 提交定制需求(公开接口,游客可提交,建议配合网关层频率限制)
|
||||
*/
|
||||
@PostMapping("/request")
|
||||
public Result<Void> submitRequest(@Valid @RequestBody CustomizationRequestDTO dto) {
|
||||
Long userId = null;
|
||||
try {
|
||||
userId = UserContext.getUserId();
|
||||
} catch (Exception ignored) {
|
||||
// 游客也可以提交定制需求
|
||||
}
|
||||
customizationRequestService.submitRequest(userId, dto);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,20 @@ package com.openclaw.module.order.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.openclaw.module.order.entity.Order;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Mapper
|
||||
public interface OrderRepository extends BaseMapper<Order> {
|
||||
@Select("SELECT COALESCE(SUM(cash_amount), 0) FROM orders WHERE status IN ('paid', 'completed')")
|
||||
BigDecimal sumTotalRevenue();
|
||||
|
||||
/** CAS方式更新订单状态(幂等性保护),返回受影响行数 */
|
||||
@Update("UPDATE orders SET status = #{newStatus}, paid_at = #{paidAt}, updated_at = NOW() WHERE id = #{orderId} AND status = #{expectedStatus} AND deleted = 0")
|
||||
int casUpdateStatus(@Param("orderId") Long orderId, @Param("expectedStatus") String expectedStatus, @Param("newStatus") String newStatus, @Param("paidAt") LocalDateTime paidAt);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -34,6 +36,8 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class OrderServiceImpl implements OrderService {
|
||||
|
||||
private static final int POINTS_RATE = 100; // 100积分=1元
|
||||
|
||||
private final OrderRepository orderRepo;
|
||||
private final OrderItemRepository orderItemRepo;
|
||||
private final OrderRefundRepository refundRepo;
|
||||
@@ -43,6 +47,54 @@ public class OrderServiceImpl implements OrderService {
|
||||
private final IdGenerator idGenerator;
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
|
||||
@Override
|
||||
public OrderPreviewVO previewOrder(Long userId, List<Long> skillIds, Integer pointsToUse) {
|
||||
// 1. 查询 Skill 价格
|
||||
List<Skill> skills = skillRepo.selectBatchIds(skillIds);
|
||||
if (skills.isEmpty()) throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
|
||||
|
||||
BigDecimal totalAmount = skills.stream()
|
||||
.map(Skill::getPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 2. 查询用户积分余额
|
||||
int availablePoints = pointsService.getBalance(userId).getAvailablePoints();
|
||||
|
||||
// 3. 计算最大可用积分
|
||||
int maxPoints = totalAmount.multiply(BigDecimal.valueOf(POINTS_RATE)).intValue();
|
||||
maxPoints = Math.min(maxPoints, availablePoints);
|
||||
|
||||
// 4. 校正 pointsToUse
|
||||
if (pointsToUse == null) pointsToUse = 0;
|
||||
pointsToUse = Math.min(Math.max(pointsToUse, 0), maxPoints);
|
||||
|
||||
// 5. 计算抵扣金额
|
||||
BigDecimal deduct = BigDecimal.valueOf(pointsToUse)
|
||||
.divide(BigDecimal.valueOf(POINTS_RATE), 2, RoundingMode.DOWN);
|
||||
BigDecimal cash = totalAmount.subtract(deduct).max(BigDecimal.ZERO);
|
||||
|
||||
// 6. 组装返回
|
||||
OrderPreviewVO vo = new OrderPreviewVO();
|
||||
vo.setItems(skills.stream().map(s -> {
|
||||
OrderItemVO item = new OrderItemVO();
|
||||
item.setSkillId(s.getId());
|
||||
item.setSkillName(s.getName());
|
||||
item.setSkillCover(s.getCoverImageUrl());
|
||||
item.setUnitPrice(s.getPrice());
|
||||
item.setQuantity(1);
|
||||
item.setTotalPrice(s.getPrice());
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
vo.setTotalAmount(totalAmount);
|
||||
vo.setPointsToUse(pointsToUse);
|
||||
vo.setPointsDeductAmount(deduct);
|
||||
vo.setCashAmount(cash);
|
||||
vo.setAvailablePoints(availablePoints);
|
||||
vo.setMaxPointsCanUse(maxPoints);
|
||||
vo.setPointsRate(POINTS_RATE);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public OrderVO createOrder(Long userId, OrderCreateDTO dto) {
|
||||
@@ -50,23 +102,42 @@ public class OrderServiceImpl implements OrderService {
|
||||
List<Skill> skills = skillRepo.selectBatchIds(dto.getSkillIds());
|
||||
if (skills.isEmpty()) throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
|
||||
|
||||
// 1.1 校验是否已拥有(防止重复购买)
|
||||
for (Skill skill : skills) {
|
||||
if (skillService.hasOwned(userId, skill.getId())) {
|
||||
throw new BusinessException(ErrorCode.SKILL_ALREADY_OWNED);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 计算总金额
|
||||
BigDecimal totalAmount = skills.stream()
|
||||
.map(Skill::getPrice)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
// 3. 处理积分抵扣
|
||||
// 3. 处理积分抵扣(校正上限,防止超额消耗)
|
||||
int pointsToUse = dto.getPointsToUse() != null ? dto.getPointsToUse() : 0;
|
||||
if (pointsToUse > 0) {
|
||||
if (!pointsService.hasEnoughPoints(userId, pointsToUse)) {
|
||||
int maxPoints = totalAmount.multiply(BigDecimal.valueOf(POINTS_RATE)).intValue();
|
||||
int availablePoints = pointsService.getBalance(userId).getAvailablePoints();
|
||||
maxPoints = Math.min(maxPoints, availablePoints);
|
||||
pointsToUse = Math.min(Math.max(pointsToUse, 0), maxPoints);
|
||||
if (pointsToUse > 0 && !pointsService.hasEnoughPoints(userId, pointsToUse)) {
|
||||
throw new BusinessException(ErrorCode.POINTS_NOT_ENOUGH);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 计算现金金额
|
||||
BigDecimal pointsDeductAmount = BigDecimal.valueOf(pointsToUse).divide(BigDecimal.valueOf(100));
|
||||
BigDecimal cashAmount = totalAmount.subtract(pointsDeductAmount);
|
||||
if (cashAmount.compareTo(BigDecimal.ZERO) < 0) cashAmount = BigDecimal.ZERO;
|
||||
// 4. 计算现金金额(修复BigDecimal除法精度)
|
||||
BigDecimal pointsDeductAmount = BigDecimal.valueOf(pointsToUse)
|
||||
.divide(BigDecimal.valueOf(POINTS_RATE), 2, RoundingMode.DOWN);
|
||||
BigDecimal cashAmount = totalAmount.subtract(pointsDeductAmount).max(BigDecimal.ZERO);
|
||||
|
||||
// 4.1 自动判定支付方式
|
||||
String paymentMethod = dto.getPaymentMethod();
|
||||
if (cashAmount.compareTo(BigDecimal.ZERO) == 0 && pointsToUse > 0) {
|
||||
paymentMethod = "points";
|
||||
} else if (pointsToUse > 0 && cashAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
paymentMethod = "mixed";
|
||||
}
|
||||
|
||||
// 5. 创建订单
|
||||
Order order = new Order();
|
||||
@@ -77,7 +148,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
order.setPointsUsed(pointsToUse);
|
||||
order.setPointsDeductAmount(pointsDeductAmount);
|
||||
order.setStatus("pending");
|
||||
order.setPaymentMethod(dto.getPaymentMethod());
|
||||
order.setPaymentMethod(paymentMethod);
|
||||
order.setExpiredAt(LocalDateTime.now().plusHours(1));
|
||||
orderRepo.insert(order);
|
||||
|
||||
@@ -99,10 +170,24 @@ public class OrderServiceImpl implements OrderService {
|
||||
pointsService.freezePoints(userId, pointsToUse, order.getId());
|
||||
}
|
||||
|
||||
// 8. 发送订单超时延迟消息(1小时后自动取消)
|
||||
// 8. 纯积分支付:直接扣减冻结积分并完成订单
|
||||
if (cashAmount.compareTo(BigDecimal.ZERO) == 0 && pointsToUse > 0) {
|
||||
pointsService.consumeFrozenPoints(userId, pointsToUse, order.getId());
|
||||
order.setStatus("completed");
|
||||
order.setPaidAt(LocalDateTime.now());
|
||||
orderRepo.updateById(order);
|
||||
// 发放 Skill 访问权限
|
||||
for (Skill skill : skills) {
|
||||
skillService.grantAccess(userId, skill.getId(), order.getId(), "points");
|
||||
}
|
||||
log.info("纯积分订单直接完成: orderId={}, points={}", order.getId(), pointsToUse);
|
||||
return toVO(order, skills);
|
||||
}
|
||||
|
||||
// 9. 非纯积分:发送订单超时延迟消息(1小时后自动取消)
|
||||
try {
|
||||
OrderTimeoutEvent timeoutEvent = new OrderTimeoutEvent(order.getId(), userId, order.getOrderNo());
|
||||
rabbitTemplate.convertAndSend(MQConstants.EXCHANGE_DELAY_DLX, MQConstants.RK_DELAY_ORDER_TIMEOUT, timeoutEvent);
|
||||
rabbitTemplate.convertAndSend(MQConstants.EXCHANGE_TOPIC, "delay.order.create", timeoutEvent);
|
||||
log.info("[MQ] 发送订单超时延迟消息: orderId={}, orderNo={}", order.getId(), order.getOrderNo());
|
||||
} catch (Exception e) {
|
||||
log.error("[MQ] 发送订单超时延迟消息失败: orderId={}", order.getId(), e);
|
||||
@@ -119,10 +204,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
}
|
||||
List<OrderItem> items = orderItemRepo.selectList(
|
||||
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderId));
|
||||
List<Skill> skills = items.stream()
|
||||
.map(item -> skillRepo.selectById(item.getSkillId()))
|
||||
.collect(Collectors.toList());
|
||||
return toVO(order, skills);
|
||||
return toVOFromItems(order, items);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,13 +214,22 @@ public class OrderServiceImpl implements OrderService {
|
||||
new LambdaQueryWrapper<Order>()
|
||||
.eq(Order::getUserId, userId)
|
||||
.orderByDesc(Order::getCreatedAt));
|
||||
|
||||
// 批量查询所有订单项(1次IN查询代替N次逐条查询)
|
||||
List<Long> orderIds = page.getRecords().stream()
|
||||
.map(Order::getId).collect(Collectors.toList());
|
||||
Map<Long, List<OrderItem>> itemsMap;
|
||||
if (!orderIds.isEmpty()) {
|
||||
List<OrderItem> allItems = orderItemRepo.selectList(
|
||||
new LambdaQueryWrapper<OrderItem>().in(OrderItem::getOrderId, orderIds));
|
||||
itemsMap = allItems.stream().collect(Collectors.groupingBy(OrderItem::getOrderId));
|
||||
} else {
|
||||
itemsMap = java.util.Collections.emptyMap();
|
||||
}
|
||||
|
||||
return page.convert(order -> {
|
||||
List<OrderItem> items = orderItemRepo.selectList(
|
||||
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, order.getId()));
|
||||
List<Skill> skills = items.stream()
|
||||
.map(item -> skillRepo.selectById(item.getSkillId()))
|
||||
.collect(Collectors.toList());
|
||||
return toVO(order, skills);
|
||||
List<OrderItem> items = itemsMap.getOrDefault(order.getId(), java.util.Collections.emptyList());
|
||||
return toVOFromItems(order, items);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,9 +243,14 @@ public class OrderServiceImpl implements OrderService {
|
||||
if (!"pending".equals(order.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR);
|
||||
}
|
||||
// CAS更新:防止并发重复支付
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int rows = orderRepo.casUpdateStatus(orderId, "pending", "paid", now);
|
||||
if (rows == 0) {
|
||||
throw new BusinessException(ErrorCode.ORDER_STATUS_ERROR);
|
||||
}
|
||||
order.setStatus("paid");
|
||||
order.setPaidAt(LocalDateTime.now());
|
||||
orderRepo.updateById(order);
|
||||
order.setPaidAt(now);
|
||||
|
||||
// 发布订单支付成功事件(异步发放Skill访问权限)
|
||||
try {
|
||||
@@ -166,8 +262,15 @@ public class OrderServiceImpl implements OrderService {
|
||||
List<OrderItem> items = orderItemRepo.selectList(
|
||||
new LambdaQueryWrapper<OrderItem>().eq(OrderItem::getOrderId, orderId));
|
||||
for (OrderItem item : items) {
|
||||
skillService.grantAccess(userId, item.getSkillId(), orderId, "purchase");
|
||||
skillService.grantAccess(userId, item.getSkillId(), orderId, "paid");
|
||||
}
|
||||
// MQ 失败降级:同步消费冻结积分
|
||||
if (order.getPointsUsed() != null && order.getPointsUsed() > 0) {
|
||||
pointsService.consumeFrozenPoints(userId, order.getPointsUsed(), orderId);
|
||||
}
|
||||
// MQ 失败降级:同步完成订单状态转换
|
||||
order.setStatus("completed");
|
||||
orderRepo.updateById(order);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +289,7 @@ public class OrderServiceImpl implements OrderService {
|
||||
orderRepo.updateById(order);
|
||||
|
||||
// 解冻积分
|
||||
if (order.getPointsUsed() > 0) {
|
||||
if (order.getPointsUsed() != null && order.getPointsUsed() > 0) {
|
||||
pointsService.unfreezePoints(userId, order.getPointsUsed(), orderId);
|
||||
}
|
||||
|
||||
@@ -252,6 +355,33 @@ public class OrderServiceImpl implements OrderService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
/** 使用OrderItem快照数据构建VO(避免N+1查询Skill表) */
|
||||
private OrderVO toVOFromItems(Order order, List<OrderItem> items) {
|
||||
OrderVO vo = new OrderVO();
|
||||
vo.setId(order.getId());
|
||||
vo.setOrderNo(order.getOrderNo());
|
||||
vo.setTotalAmount(order.getTotalAmount());
|
||||
vo.setCashAmount(order.getCashAmount());
|
||||
vo.setPointsUsed(order.getPointsUsed());
|
||||
vo.setPointsDeductAmount(order.getPointsDeductAmount());
|
||||
vo.setStatus(order.getStatus());
|
||||
vo.setStatusLabel(getStatusLabel(order.getStatus()));
|
||||
vo.setPaymentMethod(order.getPaymentMethod());
|
||||
vo.setCreatedAt(order.getCreatedAt());
|
||||
vo.setPaidAt(order.getPaidAt());
|
||||
vo.setItems(items.stream().map(oi -> {
|
||||
OrderItemVO item = new OrderItemVO();
|
||||
item.setSkillId(oi.getSkillId());
|
||||
item.setSkillName(oi.getSkillName());
|
||||
item.setSkillCover(oi.getSkillCover());
|
||||
item.setUnitPrice(oi.getUnitPrice());
|
||||
item.setQuantity(oi.getQuantity());
|
||||
item.setTotalPrice(oi.getTotalPrice());
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String getStatusLabel(String status) {
|
||||
return switch (status) {
|
||||
case "pending" -> "待支付";
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.openclaw.module.user.service.UserService;
|
||||
import com.openclaw.annotation.RequiresRole;
|
||||
import com.openclaw.util.UserContext;
|
||||
import com.openclaw.module.user.vo.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -18,12 +19,30 @@ public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
/** 发送短信验证码(注册/找回密码用) */
|
||||
@PostMapping("/sms-code")
|
||||
public Result<Void> sendSmsCode(@RequestParam String phone) {
|
||||
userService.sendSmsCode(phone);
|
||||
@PostMapping("/sms/code")
|
||||
public Result<Void> sendSmsCode(@Valid @RequestBody SmsCodeDTO dto, HttpServletRequest request) {
|
||||
String ip = getClientIp(request);
|
||||
userService.sendSmsCode(dto.getPhone(), ip);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
// 优先取 X-Real-IP(由反向代理设置,不可被客户端伪造)
|
||||
String ip = request.getHeader("X-Real-IP");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip != null && ip.contains(",")) {
|
||||
// 取最后一个IP(最近一跳代理添加的,相对可信)
|
||||
String[] parts = ip.split(",");
|
||||
ip = parts[parts.length - 1].trim();
|
||||
}
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/** 用户注册 */
|
||||
@PostMapping("/register")
|
||||
public Result<LoginVO> register(@Valid @RequestBody UserRegisterDTO dto) {
|
||||
@@ -55,27 +74,29 @@ public class UserController {
|
||||
/** 更新个人信息 */
|
||||
@RequiresRole("user")
|
||||
@PutMapping("/profile")
|
||||
public Result<UserVO> updateProfile(@RequestBody UserUpdateDTO dto) {
|
||||
public Result<UserVO> updateProfile(@Valid @RequestBody UserUpdateDTO dto) {
|
||||
return Result.ok(userService.updateProfile(UserContext.getUserId(), dto));
|
||||
}
|
||||
|
||||
@RequiresRole("user")
|
||||
@PutMapping("/phone")
|
||||
public Result<Void> changePhone(@Valid @RequestBody ChangePhoneDTO dto) {
|
||||
userService.changePhone(UserContext.getUserId(), dto);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 修改密码 */
|
||||
@RequiresRole("user")
|
||||
@PutMapping("/password")
|
||||
public Result<Void> changePassword(
|
||||
@RequestParam String oldPassword,
|
||||
@RequestParam String newPassword) {
|
||||
userService.changePassword(UserContext.getUserId(), oldPassword, newPassword);
|
||||
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordDTO dto) {
|
||||
userService.changePassword(UserContext.getUserId(), dto.getOldPassword(), dto.getNewPassword());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/** 忘记密码 - 重置 */
|
||||
@PostMapping("/password/reset")
|
||||
public Result<Void> resetPassword(
|
||||
@RequestParam String phone,
|
||||
@RequestParam String smsCode,
|
||||
@RequestParam String newPassword) {
|
||||
userService.resetPassword(phone, smsCode, newPassword);
|
||||
public Result<Void> resetPassword(@Valid @RequestBody ResetPasswordDTO dto) {
|
||||
userService.resetPassword(dto.getPhone(), dto.getSmsCode(), dto.getNewPassword());
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/openclaw?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: root
|
||||
password: ${DB_PASSWORD:177615}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
@@ -44,12 +44,12 @@ mybatis-plus:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deletedAt
|
||||
logic-delete-value: "now()"
|
||||
logic-not-delete-value: "null"
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
jwt:
|
||||
secret: change-this-to-a-256-bit-random-secret-key-for-production
|
||||
secret: ${JWT_SECRET:change-this-to-a-256-bit-random-secret-key-for-production}
|
||||
expire-ms: 86400000
|
||||
|
||||
invite:
|
||||
@@ -69,3 +69,78 @@ recharge:
|
||||
bonusPoints: 800
|
||||
- amount: 1000
|
||||
bonusPoints: 2000
|
||||
|
||||
# SpringDoc OpenAPI(生产环境应设为 false 关闭)
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /v3/api-docs
|
||||
enabled: ${SWAGGER_ENABLED:true}
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
enabled: ${SWAGGER_ENABLED:true}
|
||||
|
||||
# Actuator 健康检查(仅暴露 health 端点,生产环境禁止暴露 env/metrics/info)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health
|
||||
endpoint:
|
||||
health:
|
||||
show-details: never
|
||||
env:
|
||||
enabled: false
|
||||
configprops:
|
||||
enabled: false
|
||||
beans:
|
||||
enabled: false
|
||||
|
||||
info:
|
||||
app:
|
||||
name: OpenClaw Backend
|
||||
version: 1.0.0
|
||||
|
||||
# 腾讯云短信配置
|
||||
tencent:
|
||||
sms:
|
||||
secret-id: ${SMS_SECRET_ID:}
|
||||
secret-key: ${SMS_SECRET_KEY:}
|
||||
sdk-app-id: "1401097910"
|
||||
sign-name: openclaw
|
||||
template-id: "2612136"
|
||||
enabled: ${SMS_ENABLED:false} # 已启用真实短信发送
|
||||
|
||||
# 微信开放平台(扫码登录)
|
||||
wechat:
|
||||
open:
|
||||
app-id: ${WX_OPEN_APP_ID:your-open-app-id}
|
||||
app-secret: ${WX_OPEN_APP_SECRET:your-open-app-secret}
|
||||
redirect-uri: ${WX_OPEN_REDIRECT_URI:http://localhost:5173/auth/wechat/callback}
|
||||
enabled: ${WX_OPEN_ENABLED:false}
|
||||
|
||||
# 微信支付 V3 配置
|
||||
pay:
|
||||
app-id: your-app-id
|
||||
mch-id: your-mch-id
|
||||
api-v3-key: your-api-v3-key
|
||||
private-key-path: /path/to/apiclient_key.pem
|
||||
serial-number: your-certificate-serial-number
|
||||
notify-url: https://your-domain.com/api/v1/payments/callback/wechat
|
||||
enabled: false # 设为true启用微信支付
|
||||
|
||||
# 支付宝配置
|
||||
alipay:
|
||||
app-id: your-app-id
|
||||
private-key: your-private-key
|
||||
alipay-public-key: your-alipay-public-key
|
||||
server-url: https://openapi.alipay.com/gateway.do
|
||||
notify-url: https://your-domain.com/api/v1/payments/callback/alipay
|
||||
return-url: https://your-domain.com/payment/result
|
||||
enabled: false # 设为true启用支付宝支付
|
||||
|
||||
# 管理员账号(明文密码,启动时自动 BCrypt 编码;也支持直接配置 BCrypt hash)
|
||||
admin:
|
||||
username: ${ADMIN_USERNAME:15538239326}
|
||||
password: ${ADMIN_PASSWORD:jsx030627}
|
||||
|
||||
Reference in New Issue
Block a user