feat(topup): add admin-only audit info to top-up logs
Thread caller IP from webhook/admin controllers through model recharge functions and record a new RecordTopupLog entry with admin_info (server IP, caller IP, order payment method, callback payment method, system version). Frontend shows these fields in the expanded log row and the IP column for admins on top-up logs, while non-admins continue to see admin_info stripped by formatUserLogs.
This commit is contained in:
parent
e2807c5f95
commit
209d90e861
@ -362,7 +362,7 @@ func EpayNotify(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("易支付回调更新用户成功 %v", topUp)
|
log.Printf("易支付回调更新用户成功 %v", topUp)
|
||||||
model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money))
|
model.RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money), c.ClientIP(), topUp.PaymentMethod, "epay")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("易支付异常回调: %v", verifyInfo)
|
log.Printf("易支付异常回调: %v", verifyInfo)
|
||||||
@ -461,7 +461,7 @@ func AdminCompleteTopUp(c *gin.Context) {
|
|||||||
LockOrder(req.TradeNo)
|
LockOrder(req.TradeNo)
|
||||||
defer UnlockOrder(req.TradeNo)
|
defer UnlockOrder(req.TradeNo)
|
||||||
|
|
||||||
if err := model.ManualCompleteTopUp(req.TradeNo); err != nil {
|
if err := model.ManualCompleteTopUp(req.TradeNo, c.ClientIP()); err != nil {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -353,7 +353,7 @@ func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) {
|
|||||||
log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId)
|
log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := model.RechargeCreem(referenceId, customerEmail, customerName)
|
err := model.RechargeCreem(referenceId, customerEmail, customerName, c.ClientIP())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
|
log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
|||||||
@ -170,15 +170,16 @@ func StripeWebhook(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
callerIp := c.ClientIP()
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case stripe.EventTypeCheckoutSessionCompleted:
|
case stripe.EventTypeCheckoutSessionCompleted:
|
||||||
sessionCompleted(event)
|
sessionCompleted(event, callerIp)
|
||||||
case stripe.EventTypeCheckoutSessionExpired:
|
case stripe.EventTypeCheckoutSessionExpired:
|
||||||
sessionExpired(event)
|
sessionExpired(event)
|
||||||
case stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded:
|
case stripe.EventTypeCheckoutSessionAsyncPaymentSucceeded:
|
||||||
sessionAsyncPaymentSucceeded(event)
|
sessionAsyncPaymentSucceeded(event, callerIp)
|
||||||
case stripe.EventTypeCheckoutSessionAsyncPaymentFailed:
|
case stripe.EventTypeCheckoutSessionAsyncPaymentFailed:
|
||||||
sessionAsyncPaymentFailed(event)
|
sessionAsyncPaymentFailed(event, callerIp)
|
||||||
default:
|
default:
|
||||||
log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
|
log.Printf("不支持的Stripe Webhook事件类型: %s\n", event.Type)
|
||||||
}
|
}
|
||||||
@ -186,7 +187,7 @@ func StripeWebhook(c *gin.Context) {
|
|||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionCompleted(event stripe.Event) {
|
func sessionCompleted(event stripe.Event, callerIp string) {
|
||||||
customerId := event.GetObjectValue("customer")
|
customerId := event.GetObjectValue("customer")
|
||||||
referenceId := event.GetObjectValue("client_reference_id")
|
referenceId := event.GetObjectValue("client_reference_id")
|
||||||
status := event.GetObjectValue("status")
|
status := event.GetObjectValue("status")
|
||||||
@ -201,22 +202,22 @@ func sessionCompleted(event stripe.Event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fulfillOrder(event, referenceId, customerId)
|
fulfillOrder(event, referenceId, customerId, callerIp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionAsyncPaymentSucceeded handles delayed payment methods (bank transfer, SEPA, etc.)
|
// sessionAsyncPaymentSucceeded handles delayed payment methods (bank transfer, SEPA, etc.)
|
||||||
// that confirm payment after the checkout session completes.
|
// that confirm payment after the checkout session completes.
|
||||||
func sessionAsyncPaymentSucceeded(event stripe.Event) {
|
func sessionAsyncPaymentSucceeded(event stripe.Event, callerIp string) {
|
||||||
customerId := event.GetObjectValue("customer")
|
customerId := event.GetObjectValue("customer")
|
||||||
referenceId := event.GetObjectValue("client_reference_id")
|
referenceId := event.GetObjectValue("client_reference_id")
|
||||||
log.Printf("Stripe 异步支付成功: %s", referenceId)
|
log.Printf("Stripe 异步支付成功: %s", referenceId)
|
||||||
|
|
||||||
fulfillOrder(event, referenceId, customerId)
|
fulfillOrder(event, referenceId, customerId, callerIp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sessionAsyncPaymentFailed marks orders as failed when delayed payment methods
|
// sessionAsyncPaymentFailed marks orders as failed when delayed payment methods
|
||||||
// ultimately fail (e.g. bank transfer not received, SEPA rejected).
|
// ultimately fail (e.g. bank transfer not received, SEPA rejected).
|
||||||
func sessionAsyncPaymentFailed(event stripe.Event) {
|
func sessionAsyncPaymentFailed(event stripe.Event, callerIp string) {
|
||||||
referenceId := event.GetObjectValue("client_reference_id")
|
referenceId := event.GetObjectValue("client_reference_id")
|
||||||
log.Printf("Stripe 异步支付失败: %s", referenceId)
|
log.Printf("Stripe 异步支付失败: %s", referenceId)
|
||||||
|
|
||||||
@ -253,7 +254,7 @@ func sessionAsyncPaymentFailed(event stripe.Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fulfillOrder is the shared logic for crediting quota after payment is confirmed.
|
// fulfillOrder is the shared logic for crediting quota after payment is confirmed.
|
||||||
func fulfillOrder(event stripe.Event, referenceId string, customerId string) {
|
func fulfillOrder(event stripe.Event, referenceId string, customerId string, callerIp string) {
|
||||||
if len(referenceId) == 0 {
|
if len(referenceId) == 0 {
|
||||||
log.Println("未提供支付单号")
|
log.Println("未提供支付单号")
|
||||||
return
|
return
|
||||||
@ -274,7 +275,7 @@ func fulfillOrder(event stripe.Event, referenceId string, customerId string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := model.Recharge(referenceId, customerId)
|
err := model.Recharge(referenceId, customerId, callerIp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err.Error(), referenceId)
|
log.Println(err.Error(), referenceId)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -357,7 +357,7 @@ func handleWaffoPayment(c *gin.Context, wh *core.WebhookHandler, result *core.Pa
|
|||||||
LockOrder(merchantOrderId)
|
LockOrder(merchantOrderId)
|
||||||
defer UnlockOrder(merchantOrderId)
|
defer UnlockOrder(merchantOrderId)
|
||||||
|
|
||||||
if err := model.RechargeWaffo(merchantOrderId); err != nil {
|
if err := model.RechargeWaffo(merchantOrderId, c.ClientIP()); err != nil {
|
||||||
log.Printf("Waffo 充值处理失败: %v, 订单: %s", err, merchantOrderId)
|
log.Printf("Waffo 充值处理失败: %v, 订单: %s", err, merchantOrderId)
|
||||||
sendWaffoWebhookResponse(c, wh, false, err.Error())
|
sendWaffoWebhookResponse(c, wh, false, err.Error())
|
||||||
return
|
return
|
||||||
|
|||||||
27
model/log.go
27
model/log.go
@ -90,6 +90,33 @@ func RecordLog(userId int, logType int, content string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RecordTopupLog(userId int, content string, callerIp string, paymentMethod string, callbackPaymentMethod string) {
|
||||||
|
username, _ := GetUsernameById(userId, false)
|
||||||
|
adminInfo := map[string]interface{}{
|
||||||
|
"server_ip": common.GetIp(),
|
||||||
|
"caller_ip": callerIp,
|
||||||
|
"payment_method": paymentMethod,
|
||||||
|
"callback_payment_method": callbackPaymentMethod,
|
||||||
|
"version": common.Version,
|
||||||
|
}
|
||||||
|
other := map[string]interface{}{
|
||||||
|
"admin_info": adminInfo,
|
||||||
|
}
|
||||||
|
log := &Log{
|
||||||
|
UserId: userId,
|
||||||
|
Username: username,
|
||||||
|
CreatedAt: common.GetTimestamp(),
|
||||||
|
Type: LogTypeTopup,
|
||||||
|
Content: content,
|
||||||
|
Ip: callerIp,
|
||||||
|
Other: common.MapToJsonStr(other),
|
||||||
|
}
|
||||||
|
err := LOG_DB.Create(log).Error
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("failed to record topup log: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
|
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
|
||||||
isStream bool, group string, other map[string]interface{}) {
|
isStream bool, group string, other map[string]interface{}) {
|
||||||
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
|
logger.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
|
||||||
|
|||||||
@ -57,7 +57,7 @@ func GetTopUpByTradeNo(tradeNo string) *TopUp {
|
|||||||
return topUp
|
return topUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func Recharge(referenceId string, customerId string) (err error) {
|
func Recharge(referenceId string, customerId string, callerIp string) (err error) {
|
||||||
if referenceId == "" {
|
if referenceId == "" {
|
||||||
return errors.New("未提供支付单号")
|
return errors.New("未提供支付单号")
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ func Recharge(referenceId string, customerId string) (err error) {
|
|||||||
return errors.New("充值失败,请稍后重试")
|
return errors.New("充值失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount))
|
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%d", logger.FormatQuota(int(quota)), topUp.Amount), callerIp, topUp.PaymentMethod, "stripe")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -242,7 +242,7 @@ func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ManualCompleteTopUp 管理员手动完成订单并给用户充值
|
// ManualCompleteTopUp 管理员手动完成订单并给用户充值
|
||||||
func ManualCompleteTopUp(tradeNo string) error {
|
func ManualCompleteTopUp(tradeNo string, callerIp string) error {
|
||||||
if tradeNo == "" {
|
if tradeNo == "" {
|
||||||
return errors.New("未提供订单号")
|
return errors.New("未提供订单号")
|
||||||
}
|
}
|
||||||
@ -255,6 +255,7 @@ func ManualCompleteTopUp(tradeNo string) error {
|
|||||||
var userId int
|
var userId int
|
||||||
var quotaToAdd int
|
var quotaToAdd int
|
||||||
var payMoney float64
|
var payMoney float64
|
||||||
|
var paymentMethod string
|
||||||
|
|
||||||
err := DB.Transaction(func(tx *gorm.DB) error {
|
err := DB.Transaction(func(tx *gorm.DB) error {
|
||||||
topUp := &TopUp{}
|
topUp := &TopUp{}
|
||||||
@ -301,6 +302,7 @@ func ManualCompleteTopUp(tradeNo string) error {
|
|||||||
|
|
||||||
userId = topUp.UserId
|
userId = topUp.UserId
|
||||||
payMoney = topUp.Money
|
payMoney = topUp.Money
|
||||||
|
paymentMethod = topUp.PaymentMethod
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -309,10 +311,10 @@ func ManualCompleteTopUp(tradeNo string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 事务外记录日志,避免阻塞
|
// 事务外记录日志,避免阻塞
|
||||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney))
|
RecordTopupLog(userId, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney), callerIp, paymentMethod, "admin")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) {
|
func RechargeCreem(referenceId string, customerEmail string, customerName string, callerIp string) (err error) {
|
||||||
if referenceId == "" {
|
if referenceId == "" {
|
||||||
return errors.New("未提供支付单号")
|
return errors.New("未提供支付单号")
|
||||||
}
|
}
|
||||||
@ -382,12 +384,12 @@ func RechargeCreem(referenceId string, customerEmail string, customerName string
|
|||||||
return errors.New("充值失败,请稍后重试")
|
return errors.New("充值失败,请稍后重试")
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money))
|
RecordTopupLog(topUp.UserId, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money), callerIp, topUp.PaymentMethod, "creem")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RechargeWaffo(tradeNo string) (err error) {
|
func RechargeWaffo(tradeNo string, callerIp string) (err error) {
|
||||||
if tradeNo == "" {
|
if tradeNo == "" {
|
||||||
return errors.New("未提供支付单号")
|
return errors.New("未提供支付单号")
|
||||||
}
|
}
|
||||||
@ -444,7 +446,7 @@ func RechargeWaffo(tradeNo string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if quotaToAdd > 0 {
|
if quotaToAdd > 0 {
|
||||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money))
|
RecordTopupLog(topUp.UserId, fmt.Sprintf("Waffo充值成功,充值额度: %v,支付金额: %.2f", logger.FormatQuota(quotaToAdd), topUp.Money), callerIp, topUp.PaymentMethod, "waffo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -876,7 +876,12 @@ export const getLogsColumns = ({
|
|||||||
),
|
),
|
||||||
dataIndex: 'ip',
|
dataIndex: 'ip',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (record.type === 2 || record.type === 5) && text ? (
|
const showIp =
|
||||||
|
(record.type === 2 ||
|
||||||
|
record.type === 5 ||
|
||||||
|
(isAdminUser && record.type === 1)) &&
|
||||||
|
text;
|
||||||
|
return showIp ? (
|
||||||
<Tooltip content={text}>
|
<Tooltip content={text}>
|
||||||
<span>
|
<span>
|
||||||
<Tag
|
<Tag
|
||||||
|
|||||||
37
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
37
web/src/hooks/usage-logs/useUsageLogsData.jsx
vendored
@ -695,13 +695,13 @@ export const useLogsData = () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isAdminUser && logs[i].type !== 6) {
|
if (isAdminUser && logs[i].type !== 6 && logs[i].type !== 1) {
|
||||||
expandDataLocal.push({
|
expandDataLocal.push({
|
||||||
key: t('请求转换'),
|
key: t('请求转换'),
|
||||||
value: requestConversionDisplayValue(other?.request_conversion),
|
value: requestConversionDisplayValue(other?.request_conversion),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isAdminUser && logs[i].type !== 6) {
|
if (isAdminUser && logs[i].type !== 6 && logs[i].type !== 1) {
|
||||||
let localCountMode = '';
|
let localCountMode = '';
|
||||||
if (other?.admin_info?.local_count_tokens) {
|
if (other?.admin_info?.local_count_tokens) {
|
||||||
localCountMode = t('本地计费');
|
localCountMode = t('本地计费');
|
||||||
@ -713,6 +713,39 @@ export const useLogsData = () => {
|
|||||||
value: localCountMode,
|
value: localCountMode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (isAdminUser && logs[i].type === 1 && other?.admin_info) {
|
||||||
|
const adminInfo = other.admin_info;
|
||||||
|
if (adminInfo.payment_method) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('订单支付方式'),
|
||||||
|
value: adminInfo.payment_method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (adminInfo.callback_payment_method) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('回调支付方式'),
|
||||||
|
value: adminInfo.callback_payment_method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (adminInfo.caller_ip) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('回调调用者IP'),
|
||||||
|
value: adminInfo.caller_ip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (adminInfo.server_ip) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('服务器IP'),
|
||||||
|
value: adminInfo.server_ip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (adminInfo.version) {
|
||||||
|
expandDataLocal.push({
|
||||||
|
key: t('系统版本'),
|
||||||
|
value: adminInfo.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
expandDatesLocal[logs[i].key] = expandDataLocal;
|
expandDatesLocal[logs[i].key] = expandDataLocal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
536
web/src/i18n/locales/en.json
vendored
536
web/src/i18n/locales/en.json
vendored
File diff suppressed because it is too large
Load Diff
573
web/src/i18n/locales/fr.json
vendored
573
web/src/i18n/locales/fr.json
vendored
File diff suppressed because it is too large
Load Diff
533
web/src/i18n/locales/ja.json
vendored
533
web/src/i18n/locales/ja.json
vendored
File diff suppressed because it is too large
Load Diff
579
web/src/i18n/locales/ru.json
vendored
579
web/src/i18n/locales/ru.json
vendored
File diff suppressed because it is too large
Load Diff
538
web/src/i18n/locales/vi.json
vendored
538
web/src/i18n/locales/vi.json
vendored
File diff suppressed because it is too large
Load Diff
1490
web/src/i18n/locales/zh-CN.json
vendored
1490
web/src/i18n/locales/zh-CN.json
vendored
File diff suppressed because it is too large
Load Diff
820
web/src/i18n/locales/zh-TW.json
vendored
820
web/src/i18n/locales/zh-TW.json
vendored
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user