feat: add Go backend

This commit is contained in:
richjohnrambo
2026-10-10 20:56:17 +08:00
parent 50fcfd2d3d
commit f6024f0964
1040 changed files with 107169 additions and 0 deletions
+309
View File
@@ -0,0 +1,309 @@
package sys
import (
"context"
"fmt"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/contexts"
"hotgo/internal/library/hgorm/handler"
"hotgo/internal/library/hgorm/hook"
"hotgo/internal/model/entity"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"hotgo/utility/encrypt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
)
type sSysRechargeOrder struct{}
func NewSysRechargeOrder() *sSysRechargeOrder { return &sSysRechargeOrder{} }
func init() { service.RegisterSysRechargeOrder(NewSysRechargeOrder()) }
func (s *sSysRechargeOrder) model(ctx context.Context) *gdb.Model {
return handler.Model(dao.RechargeOrder.Ctx(ctx), &handler.Option{FilterTenant: true})
}
func (s *sSysRechargeOrder) baseModel(ctx context.Context) *gdb.Model {
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
return s.model(ctx).As("r").
LeftJoin(dao.AppMember.Table()+" m", col("m", mc.Id)+"="+col("r", rc.MemberId)).
LeftJoin(dao.AppMemberProfile.Table()+" p", col("p", pc.MemberId)+"="+col("r", rc.MemberId)).
LeftJoin(dao.WalletAccount.Table()+" w", col("w", wc.MemberId)+"="+col("r", rc.MemberId)+" AND "+col("w", wc.Currency)+"='CNY'").
LeftJoin(dao.PaymentChannel.Table()+" c", col("c", cc.Id)+"="+col("r", rc.ChannelId))
}
func (s *sSysRechargeOrder) listModel(ctx context.Context) *gdb.Model {
rc, mc, pc, wc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.AppMemberProfile.Columns(), dao.WalletAccount.Columns(), dao.PaymentChannel.Columns()
return s.baseModel(ctx).
Fields(col("r", rc.Id), col("r", rc.RechargeNo), col("r", rc.MemberId), col("m", mc.Username), col("m", mc.MobileMask), col("p", pc.RealNameMask), fmt.Sprintf("COALESCE(%s,0) available_amount", col("w", wc.AvailableAmount)), col("r", rc.ChannelId), fmt.Sprintf("COALESCE(%s,'') channel_name", col("c", cc.Name)), col("r", rc.RequestAmount), col("r", rc.PaidAmount), col("r", rc.Currency), col("r", rc.ExternalNo), col("r", rc.VoucherAttachmentIds), col("r", rc.RechargeType), col("r", rc.RechargeStatus), col("r", rc.Remark), col("r", rc.ReviewReasonCode), col("r", rc.ReviewReason), col("r", rc.ApprovalRequestId), col("r", rc.LedgerId), col("r", rc.PaidAt), col("r", rc.ReviewedAt), col("r", rc.CreditedAt), col("r", rc.CreatedAt))
}
func (s *sSysRechargeOrder) filter(ctx context.Context, mod *gdb.Model, in *sysin.RechargeOrderListInp) (*gdb.Model, error) {
rc, mc, cc := dao.RechargeOrder.Columns(), dao.AppMember.Columns(), dao.PaymentChannel.Columns()
if in.RechargeNo != "" {
mod = mod.WhereLike(col("r", rc.RechargeNo), "%"+in.RechargeNo+"%")
}
if in.Username != "" {
mod = mod.WhereLike(col("m", mc.Username), "%"+in.Username+"%")
}
if in.Mobile != "" {
hash, err := encrypt.HashField(ctx, in.Mobile)
if err != nil {
return nil, err
}
mod = mod.Where(col("m", mc.MobileHash), hash)
}
if in.RechargeStatus > 0 {
mod = mod.Where(col("r", rc.RechargeStatus), in.RechargeStatus)
}
if in.ChannelId > 0 {
mod = mod.Where(col("r", rc.ChannelId), in.ChannelId)
}
if in.ChannelName != "" {
mod = mod.WhereLike(col("c", cc.Name), "%"+in.ChannelName+"%")
}
if len(in.CreatedAt) == 2 {
mod = mod.WhereBetween(col("r", rc.CreatedAt), in.CreatedAt[0], in.CreatedAt[1])
}
return mod, nil
}
func (s *sSysRechargeOrder) List(ctx context.Context, in *sysin.RechargeOrderListInp) (list []*sysin.RechargeOrderListModel, total int, err error) {
mod, err := s.filter(ctx, s.listModel(ctx), in)
if err != nil {
return nil, 0, err
}
err = mod.Page(in.Page, in.PerPage).OrderDesc(col("r", dao.RechargeOrder.Columns().Id)).ScanAndCount(&list, &total, false)
if err != nil {
err = gerror.Wrap(err, "获取充值订单失败")
}
return
}
func (s *sSysRechargeOrder) Summary(ctx context.Context, in *sysin.RechargeOrderListInp) (res *sysin.RechargeSummaryModel, err error) {
rc := dao.RechargeOrder.Columns()
mod, err := s.filter(ctx, s.baseModel(ctx).Fields(
"COUNT(DISTINCT "+col("r", rc.Id)+") total_count",
"COALESCE(SUM("+col("r", rc.RequestAmount)+"),0) total_amount",
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) paid_count", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) paid_amount", col("r", rc.RechargeStatus), consts.RechargeSuccess, col("r", rc.PaidAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) unpaid_count", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) unpaid_amount", col("r", rc.RechargeStatus), consts.RechargeUnpaid, col("r", rc.RequestAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) review_count", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) review_amount", col("r", rc.RechargeStatus), consts.RechargeReview, col("r", rc.RequestAmount)),
fmt.Sprintf("COUNT(DISTINCT CASE WHEN %s=%d THEN %s END) rejected_count", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.Id)),
fmt.Sprintf("COALESCE(SUM(CASE WHEN %s=%d THEN %s ELSE 0 END),0) rejected_amount", col("r", rc.RechargeStatus), consts.RechargeRejected, col("r", rc.RequestAmount)),
), in)
if err != nil {
return nil, err
}
res = new(sysin.RechargeSummaryModel)
if err = mod.Scan(res); err != nil {
return nil, gerror.Wrap(err, "汇总充值订单失败")
}
return
}
func (s *sSysRechargeOrder) View(ctx context.Context, in *sysin.RechargeOrderViewInp) (res *sysin.RechargeOrderListModel, err error) {
err = s.listModel(ctx).Where(col("r", dao.RechargeOrder.Columns().Id), in.Id).Scan(&res)
if err != nil {
return nil, gerror.Wrap(err, "获取充值订单详情失败")
}
if res == nil {
return nil, gerror.New("充值订单不存在")
}
return
}
func (s *sSysRechargeOrder) Review(ctx context.Context, in *sysin.RechargeReviewInp) (res *sysin.FinanceApprovalModel, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var order *entity.RechargeOrder
if scanErr := s.model(ctx).WherePri(in.Id).LockUpdate().Scan(&order); scanErr != nil {
return scanErr
}
if order == nil {
return gerror.New("充值订单不存在")
}
if order.RechargeStatus != consts.RechargeReview {
return gerror.New("充值单已处理,请刷新列表")
}
if in.Decision != consts.FinanceDecisionApprove && in.Decision != consts.FinanceDecisionReject {
return gerror.New("审核结论不合法")
}
if in.Decision == consts.FinanceDecisionApprove {
if order.VoucherAttachmentIds == nil && order.RechargeType == consts.RechargeTypeManual {
return gerror.New("人工充值必须提供打款凭证")
}
if in.PaidAmount != order.RequestAmount {
return gerror.New("实付金额与申请金额不一致,请驳回或先核对")
}
var member *entity.AppMember
if scanErr := handler.Model(dao.AppMember.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(order.MemberId).Scan(&member); scanErr != nil {
return scanErr
}
if member == nil || member.MemberStatus != consts.AppMemberStatusNormal {
return gerror.New("会员账户状态异常,不能入账")
}
if in.ExternalNo != "" {
count, countErr := s.model(ctx).Where(dao.RechargeOrder.Columns().ExternalNo, in.ExternalNo).WhereNot(dao.RechargeOrder.Columns().Id, order.Id).Count()
if countErr != nil {
return countErr
}
if count > 0 {
return gerror.New("外部支付单号已存在,疑似重复订单")
}
}
}
rc := dao.RechargeOrder.Columns()
now := gtime.Now()
ledgerId := uint64(0)
status := consts.RechargeRejected
updateData := g.Map{
rc.RechargeStatus: status,
rc.ApprovalRequestId: 0,
rc.ReviewReasonCode: in.ReasonCode,
rc.ReviewReason: in.ReasonText,
rc.ReviewedAt: now,
rc.UpdatedBy: contexts.GetUserId(ctx),
}
if in.Decision == consts.FinanceDecisionApprove {
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if scanErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); scanErr != nil {
return scanErr
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账")
}
after := account.AvailableAmount + in.PaidAmount
lc := dao.WalletLedger.Columns()
result, insertErr := dao.WalletLedger.Ctx(ctx).
Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).
Hook(hook.SaveTenant).
Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: in.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: in.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}).
Insert()
if insertErr != nil {
return gerror.Wrap(insertErr, "写入充值台账失败")
}
id, _ := result.LastInsertId()
ledgerId = uint64(id)
walletUpdate, updateErr := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).
WherePri(account.Id).
Where(wc.Version, account.Version).
Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(in.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).
Update()
if updateErr != nil {
return updateErr
}
rows, _ := walletUpdate.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
status = consts.RechargeSuccess
updateData[rc.RechargeStatus] = status
updateData[rc.PaidAmount] = in.PaidAmount
updateData[rc.ExternalNo] = in.ExternalNo
updateData[rc.LedgerId] = ledgerId
updateData[rc.PaidAt] = now
updateData[rc.CreditedAt] = now
}
if _, updateErr := s.model(ctx).WherePri(order.Id).Data(updateData).Update(); updateErr != nil {
return updateErr
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "recharge", Action: "recharge.review", TargetType: "recharge_order", TargetId: order.Id, Before: order, After: g.Map{"decision": in.Decision, "status": status, "ledgerId": ledgerId}, Reason: in.ReasonText})
})
if err != nil {
return nil, err
}
return &sysin.FinanceApprovalModel{}, nil
}
func (s *sSysRechargeOrder) Execute(ctx context.Context, in *sysin.FinanceExecuteInp) error {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var approval *entity.ApprovalRequest
if err := service.SysApproval().Model(ctx).WherePri(in.ApprovalId).LockUpdate().Scan(&approval); err != nil {
return err
}
if approval == nil || approval.ApprovalType != "recharge_review" {
return gerror.New("充值审批不存在")
}
if approval.ApprovalStatus != consts.ApprovalStatusApproved || approval.ExecutedAt != nil {
return gerror.New("审批尚未通过或已执行")
}
var input sysin.RechargeReviewInp
if err := approval.PayloadJson.Scan(&input); err != nil {
return gerror.Wrap(err, "解析充值审批数据失败")
}
var order *entity.RechargeOrder
if err := s.model(ctx).WherePri(input.Id).LockUpdate().Scan(&order); err != nil {
return err
}
if order == nil || order.ApprovalRequestId != approval.Id || order.RechargeStatus != consts.RechargeReview {
return gerror.New("充值订单状态已变化")
}
rc := dao.RechargeOrder.Columns()
now := gtime.Now()
ledgerId := uint64(0)
if input.Decision == consts.FinanceDecisionApprove {
wc := dao.WalletAccount.Columns()
var account *entity.WalletAccount
if err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).Where(wc.MemberId, order.MemberId).Where(wc.Currency, order.Currency).LockUpdate().Scan(&account); err != nil {
return err
}
if account == nil || account.AccountStatus != 1 {
return gerror.New("会员钱包不存在或不可入账")
}
after := account.AvailableAmount + input.PaidAmount
lc := dao.WalletLedger.Columns()
result, err := dao.WalletLedger.Ctx(ctx).Fields(lc.LedgerNo, lc.AccountId, lc.MemberId, lc.BizType, lc.BizNo, lc.EntryType, lc.Direction, lc.Amount, lc.BeforeAmount, lc.AfterAmount, lc.Currency, lc.IdempotencyKey, lc.Remark, lc.OperatorId, lc.OccurredAt).Hook(hook.SaveTenant).Data(g.Map{lc.LedgerNo: "WL" + gtime.Now().Format("YmdHis") + grand.S(7), lc.AccountId: account.Id, lc.MemberId: order.MemberId, lc.BizType: "recharge", lc.BizNo: order.RechargeNo, lc.EntryType: "available", lc.Direction: consts.WalletDirectionIncome, lc.Amount: input.PaidAmount, lc.BeforeAmount: account.AvailableAmount, lc.AfterAmount: after, lc.Currency: account.Currency, lc.IdempotencyKey: fmt.Sprintf("recharge:%d", order.Id), lc.Remark: input.ReasonText, lc.OperatorId: contexts.GetUserId(ctx), lc.OccurredAt: now}).Insert()
if err != nil {
return gerror.Wrap(err, "写入充值台账失败")
}
id, _ := result.LastInsertId()
ledgerId = uint64(id)
update, err := handler.Model(dao.WalletAccount.Ctx(ctx), &handler.Option{FilterTenant: true}).WherePri(account.Id).Where(wc.Version, account.Version).Data(g.Map{wc.AvailableAmount: after, wc.TotalIncome: gdb.Raw(wc.TotalIncome + "+" + fmt.Sprint(input.PaidAmount)), wc.Version: gdb.Raw(wc.Version + "+1")}).Update()
if err != nil {
return err
}
rows, _ := update.RowsAffected()
if rows != 1 {
return gerror.New("钱包余额已变化,请重试")
}
_, err = s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeSuccess, rc.PaidAmount: input.PaidAmount, rc.ExternalNo: input.ExternalNo, rc.LedgerId: ledgerId, rc.ReviewedAt: now, rc.CreditedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update()
if err != nil {
return err
}
} else {
if _, err := s.model(ctx).WherePri(order.Id).Data(g.Map{rc.RechargeStatus: consts.RechargeRejected, rc.ReviewedAt: now, rc.UpdatedBy: contexts.GetUserId(ctx)}).Update(); err != nil {
return err
}
}
if err := recordFinanceReview(ctx, approval, "recharge", order.Id, input.Decision, input.ReasonCode, input.ReasonText, order); err != nil {
return err
}
if err := markApprovalExecuted(ctx, approval); err != nil {
return err
}
return service.SysBizAudit().Record(ctx, &sysin.BizAuditRecordInp{Module: "recharge", Action: "recharge.review.execute", TargetType: "recharge_order", TargetId: order.Id, Before: order, After: g.Map{"decision": input.Decision, "ledgerId": ledgerId}, Reason: input.ReasonText, ApprovalNo: approval.ApprovalNo})
})
}
func recordFinanceReview(ctx context.Context, approval *entity.ApprovalRequest, bizType string, bizId uint64, decision int, reasonCode, reasonText string, snapshot interface{}) error {
c := dao.FinanceReview.Columns()
_, err := dao.FinanceReview.Ctx(ctx).Fields(c.ApprovalRequestId, c.BizType, c.BizId, c.ReviewStage, c.ReviewerId, c.Decision, c.ReasonCode, c.ReasonText, c.SnapshotJson).Hook(hook.SaveTenant).Data(g.Map{c.ApprovalRequestId: approval.Id, c.BizType: bizType, c.BizId: bizId, c.ReviewStage: 2, c.ReviewerId: contexts.GetUserId(ctx), c.Decision: decision, c.ReasonCode: reasonCode, c.ReasonText: reasonText, c.SnapshotJson: gjson.New(snapshot)}).Insert()
return gerror.Wrap(err, "记录财务审核失败")
}
func markApprovalExecuted(ctx context.Context, approval *entity.ApprovalRequest) error {
c := dao.ApprovalRequest.Columns()
_, err := service.SysApproval().Model(ctx).WherePri(approval.Id).Data(g.Map{c.ApprovalStatus: consts.ApprovalStatusExecuted, c.ExecutedAt: gtime.Now(), c.UpdatedBy: contexts.GetUserId(ctx), c.Version: gdb.Raw(c.Version + "+1")}).Update()
return err
}