bindbox-game/internal/service/user/address_share.go
2025-12-26 12:22:32 +08:00

362 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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),
s.readDB.ShippingRecords.Status.Neq(5), // Ignore cancelled
).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),
s.readDB.ShippingRecords.Status.Neq(5), // Ignore cancelled
).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
}