package user import ( "context" "fmt" "time" "bindbox-game/configs" "bindbox-game/internal/repository/mysql/model" "github.com/golang-jwt/jwt/v5" ) type shareClaims struct { OwnerUserID int64 `json:"owner_user_id"` InventoryID int64 `json:"inventory_id"` jwt.RegisteredClaims } func signShareToken(ownerUserID int64, inventoryID int64, expiresAt time.Time) (string, error) { claims := shareClaims{ OwnerUserID: ownerUserID, InventoryID: inventoryID, RegisteredClaims: jwt.RegisteredClaims{ NotBefore: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(expiresAt), }, } return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(configs.Get().Random.CommitMasterKey)) } func parseShareToken(tokenString string) (*shareClaims, error) { tokenClaims, err := jwt.ParseWithClaims(tokenString, &shareClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(configs.Get().Random.CommitMasterKey), nil }) if tokenClaims != nil { if claims, ok := tokenClaims.Claims.(*shareClaims); ok && tokenClaims.Valid { return claims, nil } } return nil, err } func (s *service) CreateAddressShare(ctx context.Context, userID int64, inventoryID int64, expiresAt time.Time) (string, time.Time, error) { _, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.UserID.Eq(userID), s.readDB.UserInventory.ID.Eq(inventoryID), s.readDB.UserInventory.Status.Eq(1)).First() if err != nil { return "", time.Time{}, err } token, err := signShareToken(userID, inventoryID, expiresAt) if err != nil { return "", time.Time{}, err } return token, expiresAt, nil } func (s *service) RevokeAddressShare(ctx context.Context, userID int64, inventoryID int64) error { return nil } func (s *service) SubmitAddressShare(ctx context.Context, shareToken string, name string, mobile string, province string, city string, district string, address string, submittedByUserID *int64, submittedIP *string) (int64, error) { claims, err := parseShareToken(shareToken) if err != nil { return 0, fmt.Errorf("invalid_or_expired_token") } cnt, err := s.readDB.ShippingRecords.WithContext(ctx).Where(s.readDB.ShippingRecords.InventoryID.Eq(claims.InventoryID)).Count() if err == nil && cnt > 0 { return 0, fmt.Errorf("already_processed") } arow := &model.UserAddresses{UserID: claims.OwnerUserID, Name: name, Mobile: mobile, Province: province, City: city, District: district, Address: address, IsDefault: 0} if err := s.writeDB.UserAddresses.WithContext(ctx).Create(arow); err != nil { return 0, err } inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.ID.Eq(claims.InventoryID)).First() if err != nil { return 0, err } if inv.Status != 1 { return 0, fmt.Errorf("inventory_unavailable") } var price int64 if inv.ProductID > 0 { if p, e := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First(); e == nil && p != nil { price = p.Price } } if db := s.repo.GetDbW().Exec("INSERT INTO shipping_records (user_id, order_id, order_item_id, inventory_id, product_id, quantity, price, address_id, status, remark) VALUES (?,?,?,?,?,?,?,?,?,?)", claims.OwnerUserID, inv.OrderID, 0, claims.InventoryID, inv.ProductID, 1, price, arow.ID, 1, "shared_address_submit"); db.Error != nil { err = db.Error return 0, err } if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET status=3, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|shipping_requested') WHERE id=? AND user_id=? AND status=1", claims.InventoryID, claims.OwnerUserID); db.Error != nil { err = db.Error return 0, err } return arow.ID, nil } func (s *service) RequestShipping(ctx context.Context, userID int64, inventoryID int64) (int64, error) { return s.RequestShippingWithBatch(ctx, userID, inventoryID, "", 0) } // RequestShippingWithBatch 申请发货(支持批次号和指定地址) func (s *service) RequestShippingWithBatch(ctx context.Context, userID int64, inventoryID int64, batchNo string, addrID int64) (int64, error) { cnt, err := s.readDB.ShippingRecords.WithContext(ctx).Where(s.readDB.ShippingRecords.InventoryID.Eq(inventoryID)).Count() if err == nil && cnt > 0 { return 0, fmt.Errorf("already_processed") } inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.UserID.Eq(userID), s.readDB.UserInventory.ID.Eq(inventoryID), s.readDB.UserInventory.Status.Eq(1)).First() if err != nil { return 0, err } // 如果没有传入地址ID,使用默认地址 if addrID <= 0 { addr, err := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.UserID.Eq(userID), s.readDB.UserAddresses.IsDefault.Eq(1)).First() if err != nil { return 0, err } addrID = addr.ID } var price int64 if inv.ProductID > 0 { if p, e := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First(); e == nil && p != nil { price = p.Price } } if db := s.repo.GetDbW().Exec("INSERT INTO shipping_records (user_id, order_id, order_item_id, inventory_id, product_id, quantity, price, address_id, status, batch_no, remark) VALUES (?,?,?,?,?,?,?,?,?,?,?)", userID, inv.OrderID, 0, inventoryID, inv.ProductID, 1, price, addrID, 1, batchNo, "user_request_shipping"); db.Error != nil { err = db.Error return 0, err } if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET status=3, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|shipping_requested') WHERE id=? AND user_id=? AND status=1", inventoryID, userID); db.Error != nil { err = db.Error return 0, err } return addrID, nil } // generateBatchNo 生成唯一批次号 func generateBatchNo(userID int64) string { return fmt.Sprintf("B%d%d", userID, time.Now().UnixNano()/1000000) } func (s *service) RequestShippings(ctx context.Context, userID int64, inventoryIDs []int64, addressID *int64) (addrID int64, batchNo string, success []int64, skipped []struct { ID int64 Reason string }, failed []struct { ID int64 Reason string }, err error) { if len(inventoryIDs) == 0 { return 0, "", nil, nil, []struct { ID int64 Reason string }{{ID: 0, Reason: "invalid_params"}}, nil } dedup := make(map[int64]struct{}, len(inventoryIDs)) uniq := make([]int64, 0, len(inventoryIDs)) for _, id := range inventoryIDs { if id > 0 { if _, ok := dedup[id]; !ok { dedup[id] = struct{}{} uniq = append(uniq, id) } } } if len(uniq) == 0 { return 0, "", nil, nil, []struct { ID int64 Reason string }{{ID: 0, Reason: "invalid_params"}}, nil } if addressID != nil && *addressID > 0 { ua, _ := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.ID.Eq(*addressID), s.readDB.UserAddresses.UserID.Eq(userID)).First() if ua == nil { return 0, "", nil, nil, []struct { ID int64 Reason string }{{ID: 0, Reason: "address_not_found"}}, nil } addrID = ua.ID } else { da, e := s.readDB.UserAddresses.WithContext(ctx).Where(s.readDB.UserAddresses.UserID.Eq(userID), s.readDB.UserAddresses.IsDefault.Eq(1)).First() if e != nil || da == nil { return 0, "", nil, nil, []struct { ID int64 Reason string }{{ID: 0, Reason: "no_default_address"}}, nil } addrID = da.ID } // 始终生成批次号,方便用户查询和管理 batchNo = generateBatchNo(userID) success = make([]int64, 0, len(uniq)) skipped = make([]struct { ID int64 Reason string }, 0) failed = make([]struct { ID int64 Reason string }, 0) for _, id := range uniq { inv, _ := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.ID.Eq(id)).First() if inv == nil { skipped = append(skipped, struct { ID int64 Reason string }{ID: id, Reason: "not_found"}) continue } if inv.UserID != userID { skipped = append(skipped, struct { ID int64 Reason string }{ID: id, Reason: "not_owned"}) continue } if inv.Status == 3 { skipped = append(skipped, struct { ID int64 Reason string }{ID: id, Reason: "already_requested"}) continue } if inv.Status != 1 { skipped = append(skipped, struct { ID int64 Reason string }{ID: id, Reason: "invalid_status"}) continue } if _, err := s.RequestShippingWithBatch(ctx, userID, id, batchNo, addrID); err != nil { failed = append(failed, struct { ID int64 Reason string }{ID: id, Reason: err.Error()}) continue } success = append(success, id) } return addrID, batchNo, success, skipped, failed, nil } func (s *service) RedeemInventoryToPoints(ctx context.Context, userID int64, inventoryID int64) (int64, error) { inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.UserID.Eq(userID), s.readDB.UserInventory.ID.Eq(inventoryID), s.readDB.UserInventory.Status.Eq(1)).First() if err != nil { return 0, err } p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First() if err != nil { return 0, err } cfg, _ := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First() rate := int64(1) if cfg != nil { var r int64 _, _ = fmt.Sscanf(cfg.ConfigValue, "%d", &r) if r > 0 { rate = r } } points := p.Price * rate if err = s.AddPoints(ctx, userID, points, "redeem_reward", fmt.Sprintf("inventory:%d product:%d", inventoryID, inv.ProductID), nil, nil); err != nil { return 0, err } if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET status=3, remark=CONCAT(IFNULL(remark,''),'|redeemed_points=',?) WHERE id=? AND user_id=? AND status=1", points, inventoryID, userID); db.Error != nil { err = db.Error return 0, err } return points, nil } func (s *service) RedeemInventoriesToPoints(ctx context.Context, userID int64, inventoryIDs []int64) (int64, error) { if len(inventoryIDs) == 0 { return 0, fmt.Errorf("invalid_params") } dedup := make(map[int64]struct{}) uniq := make([]int64, 0, len(inventoryIDs)) for _, id := range inventoryIDs { if id <= 0 { continue } if _, ok := dedup[id]; !ok { dedup[id] = struct{}{} uniq = append(uniq, id) } } if len(uniq) == 0 { return 0, fmt.Errorf("invalid_params") } cfg, _ := s.readDB.SystemConfigs.WithContext(ctx).Where(s.readDB.SystemConfigs.ConfigKey.Eq("points_exchange_per_cent")).First() rate := int64(1) if cfg != nil { var r int64 _, _ = fmt.Sscanf(cfg.ConfigValue, "%d", &r) if r > 0 { rate = r } } type itemInfo struct { pid int64 pts int64 } infos := make(map[int64]itemInfo, len(uniq)) for _, id := range uniq { inv, err := s.readDB.UserInventory.WithContext(ctx).Where(s.readDB.UserInventory.UserID.Eq(userID), s.readDB.UserInventory.ID.Eq(id), s.readDB.UserInventory.Status.Eq(1)).First() if err != nil { return 0, err } p, err := s.readDB.Products.WithContext(ctx).Where(s.readDB.Products.ID.Eq(inv.ProductID)).First() if err != nil { return 0, err } points := p.Price * rate infos[id] = itemInfo{pid: inv.ProductID, pts: points} } var total int64 for _, id := range uniq { info := infos[id] if err := s.AddPoints(ctx, userID, info.pts, "redeem_reward", fmt.Sprintf("inventory:%d product:%d", id, info.pid), nil, nil); err != nil { return 0, err } if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET status=3, remark=CONCAT(IFNULL(remark,''),'|redeemed_points=',?) WHERE id=? AND user_id=? AND status=1", info.pts, id, userID); db.Error != nil { err := db.Error return 0, err } total += info.pts } return total, nil } func (s *service) VoidUserInventory(ctx context.Context, adminID int64, userID int64, inventoryID int64) error { if userID <= 0 || inventoryID <= 0 { return fmt.Errorf("invalid_params") } inv, err := s.readDB.UserInventory.WithContext(ctx). Where(s.readDB.UserInventory.ID.Eq(inventoryID)). Where(s.readDB.UserInventory.UserID.Eq(userID)). First() if err != nil { return err } if inv.Status != 1 { return fmt.Errorf("invalid_status") } if db := s.repo.GetDbW().Exec("UPDATE user_inventory SET status=2, updated_at=NOW(3), remark=CONCAT(IFNULL(remark,''),'|void_by_admin') WHERE id=? AND user_id=? AND status=1", inventoryID, userID); db.Error != nil { return db.Error } _ = adminID return nil }