This commit is contained in:
sjk
2025-11-28 15:18:10 +08:00
parent ad4a600af9
commit 5683f35942
188 changed files with 53680 additions and 1062 deletions

View File

@@ -27,13 +27,8 @@ func NewCartService(orderRepo *repository.OrderRepository, productRepo *reposito
}
// GetCart 获取购物车
// 优化: 移除用户存在性检查,因为中间件已经验证过token和用户
func (s *CartService) GetCart(userID uint) ([]model.Cart, error) {
// 检查用户是否存在
_, err := s.userRepo.GetByID(userID)
if err != nil {
return nil, errors.New("用户不存在")
}
return s.orderRepo.GetCart(userID)
}

View File

@@ -113,6 +113,15 @@ func (s *CommentService) GetCommentStats(productID uint) (*model.CommentStats, e
return s.commentRepo.GetStats(productID)
}
// GetHighRatingComments 获取高分评论(用于首页展示)
func (s *CommentService) GetHighRatingComments(limit int) ([]model.Comment, error) {
if limit <= 0 || limit > 50 {
limit = 6 // 默认6条
}
// 获取评分>=4的高分评论
return s.commentRepo.GetHighRatingComments(limit, 4)
}
// GetCommentByID 获取评论详情
func (s *CommentService) GetCommentByID(id uint) (*model.Comment, error) {
return s.commentRepo.GetByID(id)

View File

@@ -1,6 +1,7 @@
package service
import (
"context"
"dianshang/internal/model"
"dianshang/internal/repository"
"errors"
@@ -224,3 +225,338 @@ func (s *CouponService) GetAvailableCouponsForOrder(userID uint, orderAmount flo
return availableCoupons, nil
}
// ==================== 管理端方法 ====================
// GetCouponListForAdmin 获取优惠券列表(管理端)
func (s *CouponService) GetCouponListForAdmin(page, pageSize int, status, couponType, keyword string) ([]model.Coupon, int64, error) {
return s.couponRepo.GetCouponListForAdmin(page, pageSize, status, couponType, keyword)
}
// GetCouponDetailForAdmin 获取优惠券详情(管理端)
func (s *CouponService) GetCouponDetailForAdmin(couponID uint) (*model.Coupon, error) {
coupon, err := s.couponRepo.GetByID(couponID)
if err != nil {
return nil, errors.New("优惠券不存在")
}
// 获取优惠券的使用统计注意领取数和使用数不在Coupon表中而是在UserCoupon表中统计
// 这里仅作为查询不修改coupon对象
_, _, _ = s.couponRepo.GetCouponUsageStats(couponID)
return coupon, nil
}
// CreateCoupon 创建优惠券
func (s *CouponService) CreateCoupon(req interface{}, adminID uint) (*model.Coupon, error) {
// 类型断言
type CreateCouponRequest struct {
Name string
Type int
Value int64
MinAmount int64
Description string
StartTime time.Time
EndTime time.Time
TotalCount int
Status int
}
reqData, ok := req.(*CreateCouponRequest)
if !ok {
return nil, errors.New("无效的请求参数")
}
// 验证时间
if reqData.EndTime.Before(reqData.StartTime) {
return nil, errors.New("结束时间不能早于开始时间")
}
// 验证优惠券类型和值
switch reqData.Type {
case 1: // 满减券value是金额
if reqData.Value <= 0 {
return nil, errors.New("满减券优惠金额必须大于0")
}
case 2: // 折扣券value是折扣85表示8.5折)
if reqData.Value <= 0 || reqData.Value > 100 {
return nil, errors.New("折扣券折扣必须在0-100之间")
}
case 3: // 免邮券
// 免邮券不需要验证value
default:
return nil, errors.New("不支持的优惠券类型")
}
coupon := &model.Coupon{
Name: reqData.Name,
Type: uint8(reqData.Type),
Value: uint(reqData.Value),
MinAmount: uint(reqData.MinAmount),
Description: reqData.Description,
StartTime: reqData.StartTime,
EndTime: reqData.EndTime,
TotalCount: uint(reqData.TotalCount),
UsedCount: 0,
Status: uint8(reqData.Status),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.couponRepo.Create(coupon)
if err != nil {
return nil, fmt.Errorf("创建优惠券失败: %v", err)
}
return coupon, nil
}
// UpdateCoupon 更新优惠券
func (s *CouponService) UpdateCoupon(couponID uint, req interface{}) error {
// 检查优惠券是否存在
_, err := s.couponRepo.GetByID(couponID)
if err != nil {
return errors.New("优惠券不存在")
}
// 类型断言
type UpdateCouponRequest struct {
Name string
Type int
Value int64
MinAmount int64
Description string
StartTime time.Time
EndTime time.Time
TotalCount int
Status int
}
reqData, ok := req.(*UpdateCouponRequest)
if !ok {
return errors.New("无效的请求参数")
}
// 验证时间
if !reqData.EndTime.IsZero() && !reqData.StartTime.IsZero() {
if reqData.EndTime.Before(reqData.StartTime) {
return errors.New("结束时间不能早于开始时间")
}
}
updates := make(map[string]interface{})
if reqData.Name != "" {
updates["name"] = reqData.Name
}
if reqData.Type > 0 {
updates["type"] = reqData.Type
}
if reqData.Value > 0 {
updates["value"] = reqData.Value
}
if reqData.MinAmount >= 0 {
updates["min_amount"] = reqData.MinAmount
}
if reqData.Description != "" {
updates["description"] = reqData.Description
}
if !reqData.StartTime.IsZero() {
updates["start_time"] = reqData.StartTime
}
if !reqData.EndTime.IsZero() {
updates["end_time"] = reqData.EndTime
}
if reqData.TotalCount >= 0 {
updates["total_count"] = reqData.TotalCount
}
if reqData.Status >= 0 {
updates["status"] = reqData.Status
}
updates["updated_at"] = time.Now()
return s.couponRepo.Update(couponID, updates)
}
// DeleteCoupon 删除优惠券
func (s *CouponService) DeleteCoupon(couponID uint) error {
// 检查是否有用户已领取
hasUsers, err := s.couponRepo.CheckCouponHasUsers(couponID)
if err != nil {
return err
}
if hasUsers {
return errors.New("该优惠券已被用户领取,无法删除")
}
return s.couponRepo.Delete(couponID)
}
// UpdateCouponStatus 更新优惠券状态
func (s *CouponService) UpdateCouponStatus(couponID uint, status int) error {
_, err := s.couponRepo.GetByID(couponID)
if err != nil {
return errors.New("优惠券不存在")
}
updates := map[string]interface{}{
"status": status,
"updated_at": time.Now(),
}
return s.couponRepo.Update(couponID, updates)
}
// BatchDeleteCoupons 批量删除优惠券
func (s *CouponService) BatchDeleteCoupons(couponIDs []uint) error {
// 检查每个优惠券是否可以删除
for _, id := range couponIDs {
hasUsers, err := s.couponRepo.CheckCouponHasUsers(id)
if err != nil {
return err
}
if hasUsers {
return fmt.Errorf("优惠券ID %d 已被用户领取,无法删除", id)
}
}
return s.couponRepo.BatchDelete(couponIDs)
}
// GetCouponStatistics 获取优惠券统计
func (s *CouponService) GetCouponStatistics(startTime, endTime time.Time) (map[string]interface{}, error) {
ctx := context.Background()
// 获取总优惠券数
totalCoupons, err := s.couponRepo.CountTotalCoupons(ctx)
if err != nil {
return nil, err
}
// 获取启用的优惠券数
activeCoupons, err := s.couponRepo.CountActiveCoupons(ctx)
if err != nil {
return nil, err
}
// 获取总领取数
totalReceived, err := s.couponRepo.CountTotalReceived(ctx, startTime, endTime)
if err != nil {
return nil, err
}
// 获取总使用数
totalUsed, err := s.couponRepo.CountTotalUsed(ctx, startTime, endTime)
if err != nil {
return nil, err
}
// 获取各类型优惠券统计
typeStats, err := s.couponRepo.GetCouponTypeStats(ctx)
if err != nil {
return nil, err
}
// 获取热门优惠券
topCoupons, err := s.couponRepo.GetTopCoupons(ctx, 10)
if err != nil {
return nil, err
}
// 计算使用率
useRate := 0.0
if totalReceived > 0 {
useRate = float64(totalUsed) / float64(totalReceived) * 100
}
stats := map[string]interface{}{
"total_coupons": totalCoupons,
"active_coupons": activeCoupons,
"total_received": totalReceived,
"total_used": totalUsed,
"use_rate": fmt.Sprintf("%.2f%%", useRate),
"type_stats": typeStats,
"top_coupons": topCoupons,
}
return stats, nil
}
// GetUserCouponListForAdmin 获取用户优惠券列表(管理端)
func (s *CouponService) GetUserCouponListForAdmin(userID uint, page, pageSize int) ([]model.UserCoupon, int64, error) {
return s.couponRepo.GetUserCouponListForAdmin(userID, page, pageSize)
}
// DistributeCoupon 发放优惠券
func (s *CouponService) DistributeCoupon(couponID uint, userIDs []uint, distributeAll bool, quantity int, adminID uint) (map[string]interface{}, error) {
ctx := context.Background()
// 验证优惠券是否存在
coupon, err := s.couponRepo.GetByID(couponID)
if err != nil {
return nil, errors.New("优惠券不存在")
}
// 检查优惠券状态
if coupon.Status != 1 {
return nil, errors.New("优惠券已禁用,无法发放")
}
// 如果是全员发放获取所有用户ID
var targetUserIDs []uint
distributeType := "single"
if distributeAll {
distributeType = "all"
// TODO: 从用户表获取所有用户ID
// targetUserIDs, err = s.userRepo.GetAllUserIDs()
// 暂时返回错误,需要注入 userRepo
return nil, errors.New("全员发放功能暂未实现")
} else {
targetUserIDs = userIDs
if len(targetUserIDs) > 1 {
distributeType = "batch"
}
}
// 记录发放结果
successCount := 0
failCount := 0
// 给每个用户发放优惠券
for _, userID := range targetUserIDs {
for i := 0; i < quantity; i++ {
userCoupon := &model.UserCoupon{
UserID: userID,
CouponID: couponID,
Status: 0, // 未使用
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.couponRepo.CreateUserCoupon(userCoupon)
if err != nil {
failCount++
} else {
successCount++
}
}
}
// TODO: 记录发放历史到数据库
// 需要创建 coupon_distribute_history 表
_ = ctx
_ = adminID
result := map[string]interface{}{
"total_count": len(targetUserIDs) * quantity,
"success_count": successCount,
"fail_count": failCount,
"distribute_type": distributeType,
}
return result, nil
}
// GetDistributeHistory 获取发放历史
func (s *CouponService) GetDistributeHistory(page, pageSize int) ([]map[string]interface{}, int64, error) {
return s.couponRepo.GetDistributeHistory(page, pageSize)
}

View File

@@ -0,0 +1,144 @@
package service
import (
"dianshang/internal/model"
"dianshang/internal/repository"
"errors"
)
type LiveStreamService interface {
GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error)
GetLiveStreamByID(id uint) (*model.LiveStream, error)
GetActiveLiveStreams() ([]model.LiveStream, error)
CreateLiveStream(stream *model.LiveStream) error
UpdateLiveStream(id uint, stream *model.LiveStream) error
UpdateLiveStreamStatus(id uint, status int) error
DeleteLiveStream(id uint) error
BatchDeleteLiveStreams(ids []uint) error
IncrementViewCount(id uint) error
}
type liveStreamService struct {
liveStreamRepo repository.LiveStreamRepository
}
func NewLiveStreamService(liveStreamRepo repository.LiveStreamRepository) LiveStreamService {
return &liveStreamService{
liveStreamRepo: liveStreamRepo,
}
}
// GetLiveStreamList 获取投流源列表
func (s *liveStreamService) GetLiveStreamList(page, pageSize int, title, platform string, status *int) ([]model.LiveStream, int64, error) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
return s.liveStreamRepo.GetList(page, pageSize, title, platform, status)
}
// GetLiveStreamByID 获取投流源详情
func (s *liveStreamService) GetLiveStreamByID(id uint) (*model.LiveStream, error) {
if id == 0 {
return nil, errors.New("无效的投流源ID")
}
return s.liveStreamRepo.GetByID(id)
}
// GetActiveLiveStreams 获取所有启用的投流源
func (s *liveStreamService) GetActiveLiveStreams() ([]model.LiveStream, error) {
return s.liveStreamRepo.GetActiveLiveStreams()
}
// CreateLiveStream 创建投流源
func (s *liveStreamService) CreateLiveStream(stream *model.LiveStream) error {
if stream.Title == "" {
return errors.New("投流源标题不能为空")
}
if stream.Platform == "" {
return errors.New("平台名称不能为空")
}
if stream.StreamURL == "" {
return errors.New("投流URL不能为空")
}
// 检查该平台是否已有投流源
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
if err != nil {
return errors.New("检查平台投流源失败")
}
if len(existingStreams) > 0 {
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
}
return s.liveStreamRepo.Create(stream)
}
// UpdateLiveStream 更新投流源
func (s *liveStreamService) UpdateLiveStream(id uint, stream *model.LiveStream) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
// 检查投流源是否存在
existing, err := s.liveStreamRepo.GetByID(id)
if err != nil {
return errors.New("投流源不存在")
}
// 如果修改了平台,检查新平台是否已有其他投流源
if stream.Platform != "" && stream.Platform != existing.Platform {
existingStreams, _, err := s.liveStreamRepo.GetList(1, 1, "", stream.Platform, nil)
if err != nil {
return errors.New("检查平台投流源失败")
}
if len(existingStreams) > 0 {
return errors.New("该平台已存在投流源,一个平台只能设置一个投流源")
}
}
return s.liveStreamRepo.Update(id, stream)
}
// UpdateLiveStreamStatus 更新投流源状态
func (s *liveStreamService) UpdateLiveStreamStatus(id uint, status int) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
if status != 0 && status != 1 {
return errors.New("无效的状态值")
}
return s.liveStreamRepo.UpdateStatus(id, status)
}
// DeleteLiveStream 删除投流源
func (s *liveStreamService) DeleteLiveStream(id uint) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
return s.liveStreamRepo.Delete(id)
}
// BatchDeleteLiveStreams 批量删除投流源
func (s *liveStreamService) BatchDeleteLiveStreams(ids []uint) error {
if len(ids) == 0 {
return errors.New("请选择要删除的投流源")
}
return s.liveStreamRepo.BatchDelete(ids)
}
// IncrementViewCount 增加观看次数
func (s *liveStreamService) IncrementViewCount(id uint) error {
if id == 0 {
return errors.New("无效的投流源ID")
}
return s.liveStreamRepo.IncrementViewCount(id)
}

View File

@@ -0,0 +1,150 @@
package service
import (
"dianshang/internal/model"
"dianshang/internal/repository"
"dianshang/pkg/utils"
"errors"
)
// PlatformService 平台服务
type PlatformService struct {
platformRepo *repository.PlatformRepository
productRepo *repository.ProductRepository
}
// NewPlatformService 创建平台服务
func NewPlatformService(platformRepo *repository.PlatformRepository, productRepo *repository.ProductRepository) *PlatformService {
return &PlatformService{
platformRepo: platformRepo,
productRepo: productRepo,
}
}
// GetPlatformList 获取平台列表
func (s *PlatformService) GetPlatformList(page, pageSize int, status *int, name string) ([]model.Platform, *utils.Pagination, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 10
}
// 如果不需要分页,获取所有平台
if page == 0 && pageSize == 0 {
platforms, err := s.platformRepo.GetAll()
return platforms, nil, err
}
offset := (page - 1) * pageSize
// TODO: 实现带筛选的分页查询
// 暂时先返回所有平台
platforms, err := s.platformRepo.GetAll()
if err != nil {
return nil, nil, err
}
// 简单筛选
var filteredPlatforms []model.Platform
for _, p := range platforms {
if status != nil && p.Status != *status {
continue
}
if name != "" && p.Name != name && p.Code != name {
continue
}
filteredPlatforms = append(filteredPlatforms, p)
}
total := len(filteredPlatforms)
// 分页
start := offset
end := offset + pageSize
if start > total {
start = total
}
if end > total {
end = total
}
result := filteredPlatforms[start:end]
pagination := utils.NewPagination(page, pageSize)
pagination.Total = int64(total)
return result, pagination, nil
}
// GetPlatformByID 根据ID获取平台
func (s *PlatformService) GetPlatformByID(id uint) (*model.Platform, error) {
return s.platformRepo.GetByID(id)
}
// GetPlatformByCode 根据代码获取平台
func (s *PlatformService) GetPlatformByCode(code string) (*model.Platform, error) {
return s.platformRepo.GetByCode(code)
}
// CreatePlatform 创建平台
func (s *PlatformService) CreatePlatform(platform *model.Platform) error {
// 检查平台代码是否已存在
existing, _ := s.platformRepo.GetByCode(platform.Code)
if existing != nil {
return errors.New("平台代码已存在")
}
return s.platformRepo.Create(platform)
}
// UpdatePlatform 更新平台
func (s *PlatformService) UpdatePlatform(id uint, updates map[string]interface{}) error {
// 检查平台是否存在
_, err := s.platformRepo.GetByID(id)
if err != nil {
return errors.New("平台不存在")
}
// 如果更新代码,检查是否与其他平台冲突
if code, ok := updates["code"].(string); ok {
existing, _ := s.platformRepo.GetByCode(code)
if existing != nil && existing.ID != id {
return errors.New("平台代码已被其他平台使用")
}
}
return s.platformRepo.Update(id, updates)
}
// DeletePlatform 删除平台
func (s *PlatformService) DeletePlatform(id uint) error {
// 检查平台是否存在
_, err := s.platformRepo.GetByID(id)
if err != nil {
return errors.New("平台不存在")
}
// TODO: 检查是否有分类关联到该平台
// 可以选择级联删除或禁止删除
return s.platformRepo.Delete(id)
}
// GetAllActivePlatforms 获取所有启用的平台
func (s *PlatformService) GetAllActivePlatforms() ([]model.Platform, error) {
platforms, err := s.platformRepo.GetAll()
if err != nil {
return nil, err
}
// 过滤启用的平台
var activePlatforms []model.Platform
for _, p := range platforms {
if p.Status == 1 {
activePlatforms = append(activePlatforms, p)
}
}
return activePlatforms, nil
}

View File

@@ -25,7 +25,7 @@ func NewProductService(productRepo *repository.ProductRepository, userRepo *repo
}
// GetProductList 获取产品列表(前端用户)
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, keyword string, minPrice, maxPrice float64, inStock *bool, sort, sortType string) ([]model.Product, *utils.Pagination, error) {
if page <= 0 {
page = 1
}
@@ -48,6 +48,9 @@ func (s *ProductService) GetProductList(page, pageSize int, categoryID uint, key
if maxPrice > 0 {
conditions["max_price"] = maxPrice
}
if inStock != nil {
conditions["in_stock"] = *inStock
}
if sort != "" {
conditions["sort"] = sort
}
@@ -128,10 +131,12 @@ func (s *ProductService) GetProductDetail(id uint) (*model.Product, error) {
// CreateProduct 创建产品
func (s *ProductService) CreateProduct(product *model.Product) error {
// 验证分类是否存在
if product.CategoryID > 0 {
_, err := s.productRepo.GetCategoryByID(product.CategoryID)
if err != nil {
return errors.New("分类不存在")
if len(product.CategoryID) > 0 {
for _, catID := range product.CategoryID {
_, err := s.productRepo.GetCategoryByID(catID)
if err != nil {
return errors.New("分类ID" + strconv.Itoa(int(catID)) + "不存在")
}
}
}
@@ -146,28 +151,6 @@ func (s *ProductService) UpdateProduct(id uint, updates map[string]interface{})
return errors.New("产品不存在")
}
// 如果更新分类,验证分类是否存在
if categoryID, ok := updates["category_id"]; ok {
var catID uint
switch v := categoryID.(type) {
case uint:
catID = v
case float64:
catID = uint(v)
case int:
catID = uint(v)
default:
return errors.New("分类ID格式错误")
}
if catID > 0 {
_, err := s.productRepo.GetCategoryByID(catID)
if err != nil {
return errors.New("分类不存在")
}
}
}
// 处理 detail_images 字段 - 确保正确转换为 JSONSlice 类型
if detailImages, ok := updates["detail_images"]; ok {
switch v := detailImages.(type) {
@@ -376,6 +359,11 @@ func (s *ProductService) GetCategories() ([]model.Category, error) {
return s.productRepo.GetCategories()
}
// GetCategoriesByPlatform 根据平台获取分类列表
func (s *ProductService) GetCategoriesByPlatform(platform string) ([]model.Category, error) {
return s.productRepo.GetCategoriesByPlatform(platform)
}
// CreateCategory 创建分类
func (s *ProductService) CreateCategory(category *model.Category) error {
return s.productRepo.CreateCategory(category)
@@ -492,7 +480,8 @@ func (s *ProductService) SearchProducts(keyword string, page, pageSize int, minP
return []model.Product{}, utils.NewPagination(page, pageSize), nil
}
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, sort, sortType)
// 搜索不筛选库存,传递 nil
return s.GetProductList(page, pageSize, 0, keyword, minPrice, maxPrice, nil, sort, sortType)
}
// UpdateStock 更新库存

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@@ -63,6 +64,102 @@ func (s *UserService) WeChatLogin(code string) (*model.User, string, error) {
return user, token, nil
}
// EmailLogin 邮箱登录
func (s *UserService) EmailLogin(email, password, clientIP, userAgent string) (*model.User, string, error) {
// 查找用户
user, err := s.userRepo.GetByEmail(email)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, "", errors.New("邮箱或密码错误")
}
return nil, "", err
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return nil, "", errors.New("邮箱或密码错误")
}
// 检查用户状态
if user.Status == 0 {
return nil, "", errors.New("用户已被禁用")
}
// 生成JWT token (7天有效期)
tokenExpiry := 7 * 24 * 3600
token, err := jwt.GenerateToken(user.ID, "user", tokenExpiry)
if err != nil {
return nil, "", errors.New("生成token失败")
}
// 记录登录日志
s.logUserLogin(user.ID, "email", true, "", clientIP, userAgent)
return user, token, nil
}
// EmailRegister 邮箱注册
func (s *UserService) EmailRegister(email, password, nickname string) (*model.User, error) {
// 检查邮箱是否已注册
_, err := s.userRepo.GetByEmail(email)
if err == nil {
// 找到了用户,说明邮箱已注册
return nil, errors.New("该邮箱已被注册")
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
// 其他数据库错误
return nil, err
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, errors.New("密码加密失败")
}
// 创建用户
// 为邮箱用户生成唯一的 OpenID使用 email: 前缀避免与微信 OpenID 冲突)
user := &model.User{
OpenID: "email:" + email, // 使用邮箱作为 OpenID避免唯一索引冲突
Email: email,
Password: string(hashedPassword),
Nickname: nickname,
Status: 1,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
// logUserLogin 记录用户登录日志
func (s *UserService) logUserLogin(userID uint, loginType string, success bool, errorMsg, ip, userAgent string) {
status := 1
if !success {
status = 0
}
remark := loginType + " 登录"
if errorMsg != "" {
remark = errorMsg
}
log := &model.UserLoginLog{
UserID: userID,
LoginIP: ip,
UserAgent: userAgent,
LoginTime: time.Now(),
Status: status,
Remark: remark,
}
if err := s.db.Create(log).Error; err != nil {
fmt.Printf("记录登录日志失败: %v\n", err)
}
}
// CreateUser 创建用户
func (s *UserService) CreateUser(user *model.User) error {
// 检查用户是否已存在

View File

@@ -17,6 +17,7 @@ import (
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
wechatutils "github.com/wechatpay-apiv3/wechatpay-go/utils"
)
@@ -25,6 +26,7 @@ type WeChatPayService struct {
config *config.WeChatPayConfig
client *core.Client
jsapiSvc *jsapi.JsapiApiService
nativeSvc *native.NativeApiService
refundSvc *refunddomestic.RefundsApiService
privateKey *rsa.PrivateKey
orderRepo *repository.OrderRepository
@@ -74,6 +76,9 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
// 创建JSAPI服务
jsapiSvc := &jsapi.JsapiApiService{Client: client}
// 创建Native扫码支付服务
nativeSvc := &native.NativeApiService{Client: client}
// 创建退款服务
refundSvc := &refunddomestic.RefundsApiService{Client: client}
@@ -86,6 +91,7 @@ func NewWeChatPayService(cfg *config.WeChatPayConfig, orderRepo *repository.Orde
config: cfg,
client: client,
jsapiSvc: jsapiSvc,
nativeSvc: nativeSvc,
refundSvc: refundSvc,
privateKey: privateKey,
orderRepo: orderRepo,
@@ -174,6 +180,99 @@ func (s *WeChatPayService) CreateOrder(ctx context.Context, order *model.Order,
}, nil
}
// CreateNativeOrder 创建Native扫码支付订单用于PC端
func (s *WeChatPayService) CreateNativeOrder(ctx context.Context, order *model.Order) (*WeChatPayResponse, error) {
// 生成唯一的微信支付订单号
wechatOutTradeNo := utils.GenerateWechatOutTradeNo()
logger.Info("开始创建Native扫码支付订单",
"orderNo", order.OrderNo,
"wechatOutTradeNo", wechatOutTradeNo,
"totalAmount", order.TotalAmount,
"hasClient", s.client != nil)
// 更新订单的微信支付订单号
err := s.orderRepo.UpdateByOrderNo(order.OrderNo, map[string]interface{}{
"wechat_out_trade_no": wechatOutTradeNo,
"updated_at": time.Now(),
})
if err != nil {
logger.Error("更新订单微信支付订单号失败", "error", err, "orderNo", order.OrderNo)
return nil, fmt.Errorf("更新订单失败: %v", err)
}
// 如果没有客户端(开发环境),使用模拟数据
if s.client == nil {
logger.Warn("开发环境下使用模拟Native支付数据")
return s.createMockNativePayment(order)
}
// 构建Native预支付请求
req := native.PrepayRequest{
Appid: core.String(s.config.AppID),
Mchid: core.String(s.config.MchID),
Description: core.String(fmt.Sprintf("订单号: %s", order.OrderNo)),
OutTradeNo: core.String(wechatOutTradeNo),
NotifyUrl: core.String(s.config.NotifyURL),
Amount: &native.Amount{
Total: core.Int64(int64(order.TotalAmount)),
Currency: core.String("CNY"),
},
}
// 调用Native预支付API
resp, result, err := s.nativeSvc.Prepay(ctx, req)
if err != nil {
log.Printf("call Native Prepay err:%s", err)
logger.Error("创建Native支付订单失败", "error", err, "orderNo", order.OrderNo)
return nil, fmt.Errorf("创建Native支付订单失败: %v", err)
}
if result.Response.StatusCode != 200 {
log.Printf("Native Prepay status=%d", result.Response.StatusCode)
return nil, fmt.Errorf("Native预支付请求失败状态码: %d", result.Response.StatusCode)
}
log.Printf("Native Prepay success, code_url=%s", *resp.CodeUrl)
logger.Info("微信Native支付API响应",
"codeUrl", *resp.CodeUrl,
"orderNo", order.OrderNo)
return &WeChatPayResponse{
Code: 0,
Message: "success",
Data: map[string]interface{}{
"qrcode_url": *resp.CodeUrl,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
},
}, nil
}
// createMockNativePayment 创建模拟Native支付数据开发环境使用
func (s *WeChatPayService) createMockNativePayment(order *model.Order) (*WeChatPayResponse, error) {
// 生成模拟的二维码URL
mockQRCodeURL := fmt.Sprintf("weixin://wxpay/bizpayurl?pr=mock_%s", utils.GenerateRandomString(10))
logger.Info("生成模拟Native支付参数",
"environment", s.config.Environment,
"qrcodeUrl", mockQRCodeURL,
"orderNo", order.OrderNo,
"totalAmount", order.TotalAmount)
return &WeChatPayResponse{
Code: 0,
Message: "模拟支付创建成功",
Data: map[string]interface{}{
"qrcode_url": mockQRCodeURL,
"order_no": order.OrderNo,
"amount": order.TotalAmount,
"sandbox": true,
"tips": "这是模拟环境的Native支付请使用测试接口模拟支付成功",
},
}, nil
}
// createMockPayment 创建模拟支付数据(沙盒环境使用)
func (s *WeChatPayService) createMockPayment(order *model.Order, openID string) (*WeChatPayResponse, error) {
mockPrepayID := fmt.Sprintf("wx%d%s", time.Now().Unix(), generateNonceStr()[:8])