Files
hi-server/internal/logic/admin/user/updateUserBasicInfoLogic.go
T
shanshanzhong147 be09a115ec 新功能: withdrawal_log 接口加 summary 字段 + admin 禁直接扣减 commission
接口侧:
- /v1/public/user/withdrawal_log 响应新增 summary 字段, 包含:
  - commission_balance (当前余额)
  - locked_by_pending (待审批占用)
  - available_to_withdraw (可提现)
  - total_historical_amount (已通过提现总额)
  - total_refunded_amount (退款回扣总额)
  - total_income_amount (收入总额)
- 前端可据此自洽展示账目对账, 用户能在一个接口里看清整笔账

代码守护:
- updateUserBasicInfoLogic 拒绝任何 change<0 的 commission 修改
- 扣减必须走 approveWithdrawal 写 type=334 日志
- 防止未来 admin 误操作再次造成 user.commission 与 system_logs 失衡

时间戳修复:
- queryWithdrawalLogLogic 和 queryCommissionReturnLogLogic 的时间戳
  从 UnixMilli 改回 Unix (秒级), 符合项目"后端统一秒级"约定

测试:
- 补 buildSummary 的 4 个 mock 查询期望
- 加 summary 字段值正确性断言
2026-06-12 19:49:03 -07:00

182 lines
5.5 KiB
Go

package user
import (
"context"
"os"
"strings"
"time"
logicCommon "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type UpdateUserBasicInfoLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewUpdateUserBasicInfoLogic Update user basic info
func NewUpdateUserBasicInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserBasicInfoLogic {
return &UpdateUserBasicInfoLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasiceInfoRequest) error {
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId)
if err != nil {
l.Errorw("[UpdateUserBasicInfoLogic] Find User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find User Error")
}
isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo"
if req.Avatar != "" && !tool.IsValidImageSize(req.Avatar, 1024) {
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Invalid Image Size")
}
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
if req.Balance != nil && userInfo.Balance != *req.Balance {
change := *req.Balance - userInfo.Balance
balanceLog := log.Balance{
Type: log.BalanceTypeAdjust,
Amount: change,
OrderNo: "",
Balance: *req.Balance,
Timestamp: time.Now().UnixMilli(),
}
content, _ := balanceLog.Marshal()
err = tx.Create(&log.SystemLog{
Type: log.TypeBalance.Uint8(),
Date: time.Now().Format(time.DateOnly),
ObjectID: userInfo.Id,
Content: string(content),
}).Error
if err != nil {
return err
}
userInfo.Balance = *req.Balance
}
if req.GiftAmount != nil && userInfo.GiftAmount != *req.GiftAmount {
change := *req.GiftAmount - userInfo.GiftAmount
if change != 0 {
var changeType uint16
if userInfo.GiftAmount < *req.GiftAmount {
changeType = log.GiftTypeIncrease
} else {
changeType = log.GiftTypeReduce
}
giftLog := log.Gift{
Type: changeType,
Amount: change,
Balance: *req.GiftAmount,
Remark: "Admin adjustment",
Timestamp: time.Now().UnixMilli(),
}
content, _ := giftLog.Marshal()
// Add gift amount change log
err = tx.Create(&log.SystemLog{
Type: log.TypeGift.Uint8(),
Date: time.Now().Format(time.DateOnly),
ObjectID: userInfo.Id,
Content: string(content),
}).Error
if err != nil {
return err
}
userInfo.GiftAmount = *req.GiftAmount
}
}
if req.Commission != nil && *req.Commission != userInfo.Commission {
remark := ""
if req.Remark != nil {
remark = *req.Remark
}
if isWithdrawalScene(remark) {
logWithdrawalGuard(l.Logger, userInfo.Id)
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
}
change := *req.Commission - userInfo.Commission
if change < 0 {
// 禁止直接扣减佣金:扣减必须走 approveWithdrawal 写 type=334 日志,
// 否则 user.commission 和 system_logs 会再次失衡,破坏账目闭环。
l.Logger.Errorw("blocked direct commission deduction via admin update",
logger.Field("user_id", userInfo.Id),
logger.Field("change", change),
)
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess),
"commission deduction must go through withdrawal approval, not direct edit")
}
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
return err
}
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
return err
}
userInfo.Commission = *req.Commission
}
if req.Avatar != "" {
userInfo.Avatar = req.Avatar
}
if req.ReferCode != "" {
userInfo.ReferCode = req.ReferCode
}
if req.RefererId != nil {
userInfo.RefererId = *req.RefererId
}
if req.Enable != nil {
userInfo.Enable = req.Enable
}
if req.IsAdmin != nil {
userInfo.IsAdmin = req.IsAdmin
}
if req.Remark != nil {
userInfo.Remark = *req.Remark
}
if req.OnlyFirstPurchase != nil {
userInfo.OnlyFirstPurchase = req.OnlyFirstPurchase
}
if req.ReferralPercentage != 0 {
userInfo.ReferralPercentage = req.ReferralPercentage
}
if req.Password != "" {
if userInfo.Id == 2 && isDemo {
return errors.New("Demo mode does not allow modification of the admin user password")
}
userInfo.Password = tool.EncodePassWord(req.Password)
userInfo.Algo = "default"
}
err = l.svcCtx.UserModel.Update(l.ctx, userInfo, tx)
if err != nil {
return err
}
return nil
})
if err != nil {
l.Errorw("[UpdateUserBasicInfoLogic] Update User Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId))
if err.Error() == "Demo mode does not allow modification of the admin user password" {
return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow modification of the admin user password"), "UpdateUserBasicInfo failed: cannot update admin user password in demo mode")
}
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update User Error")
}
return nil
}