支持自定义线下出款分类
This commit is contained in:
@@ -80,6 +80,7 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
|
|||||||
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
||||||
ro.offline_settlement_remark AS remark,
|
ro.offline_settlement_remark AS remark,
|
||||||
'' AS category,
|
'' AS category,
|
||||||
|
'' AS custom_category_name,
|
||||||
'' AS voucher_url,
|
'' AS voucher_url,
|
||||||
NULL AS voided_at,
|
NULL AS voided_at,
|
||||||
NULL AS voided_by,
|
NULL AS voided_by,
|
||||||
@@ -125,6 +126,7 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
|
|||||||
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
COALESCE(NULLIF(operator.nickname, ''), operator.username, '') AS operator_name,
|
||||||
CASE WHEN wr.payment_remark <> '' THEN wr.payment_remark ELSE wr.review_remark END AS remark,
|
CASE WHEN wr.payment_remark <> '' THEN wr.payment_remark ELSE wr.review_remark END AS remark,
|
||||||
'' AS category,
|
'' AS category,
|
||||||
|
'' AS custom_category_name,
|
||||||
'' AS voucher_url,
|
'' AS voucher_url,
|
||||||
NULL AS voided_at,
|
NULL AS voided_at,
|
||||||
NULL AS voided_by,
|
NULL AS voided_by,
|
||||||
@@ -160,6 +162,7 @@ func (r *Repository) disbursementListBaseQuery(ctx context.Context) *gorm.DB {
|
|||||||
COALESCE(NULLIF(creator.nickname, ''), creator.username, '') AS operator_name,
|
COALESCE(NULLIF(creator.nickname, ''), creator.username, '') AS operator_name,
|
||||||
md.remark,
|
md.remark,
|
||||||
md.category,
|
md.category,
|
||||||
|
md.custom_category_name,
|
||||||
md.voucher_url,
|
md.voucher_url,
|
||||||
md.voided_at,
|
md.voided_at,
|
||||||
md.voided_by,
|
md.voided_by,
|
||||||
@@ -202,37 +205,38 @@ func (r *Repository) applyDisbursementFilters(db *gorm.DB, query DisbursementQue
|
|||||||
}
|
}
|
||||||
|
|
||||||
type disbursementItemRow struct {
|
type disbursementItemRow struct {
|
||||||
SourceType string
|
SourceType string
|
||||||
SourceID uint64
|
SourceID uint64
|
||||||
BusinessNo string
|
BusinessNo string
|
||||||
OrderID uint64
|
OrderID uint64
|
||||||
WithdrawalID uint64
|
WithdrawalID uint64
|
||||||
UserID uint64
|
UserID uint64
|
||||||
PayeeName string
|
PayeeName string
|
||||||
PayeePhone string
|
PayeePhone string
|
||||||
UploaderName string
|
UploaderName string
|
||||||
SourceChannel string
|
SourceChannel string
|
||||||
AccountType string
|
AccountType string
|
||||||
AccountName string
|
AccountName string
|
||||||
AccountNo string
|
AccountNo string
|
||||||
BankName string
|
BankName string
|
||||||
AmountCent int64
|
AmountCent int64
|
||||||
FeeCent int64
|
FeeCent int64
|
||||||
ActualAmountCent int64
|
ActualAmountCent int64
|
||||||
Status string
|
Status string
|
||||||
RawStatus string
|
RawStatus string
|
||||||
BusinessCreatedAt *time.Time
|
BusinessCreatedAt *time.Time
|
||||||
PaidAt *time.Time
|
PaidAt *time.Time
|
||||||
OperatorID *uint64
|
OperatorID *uint64
|
||||||
OperatorName string
|
OperatorName string
|
||||||
Remark string
|
Remark string
|
||||||
Category string
|
Category string
|
||||||
VoucherURL string
|
CustomCategoryName string
|
||||||
VoidedAt *time.Time
|
VoucherURL string
|
||||||
VoidedBy *uint64
|
VoidedAt *time.Time
|
||||||
VoidedByName string
|
VoidedBy *uint64
|
||||||
VoidReason string
|
VoidedByName string
|
||||||
SourcePayload []byte
|
VoidReason string
|
||||||
|
SourcePayload []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r disbursementItemRow) toDTO() DisbursementItemDTO {
|
func (r disbursementItemRow) toDTO() DisbursementItemDTO {
|
||||||
@@ -246,35 +250,36 @@ func (r disbursementItemRow) toDTO() DisbursementItemDTO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return DisbursementItemDTO{
|
return DisbursementItemDTO{
|
||||||
SourceType: r.SourceType,
|
SourceType: r.SourceType,
|
||||||
SourceID: r.SourceID,
|
SourceID: r.SourceID,
|
||||||
BusinessNo: r.BusinessNo,
|
BusinessNo: r.BusinessNo,
|
||||||
OrderID: r.OrderID,
|
OrderID: r.OrderID,
|
||||||
WithdrawalID: r.WithdrawalID,
|
WithdrawalID: r.WithdrawalID,
|
||||||
UserID: r.UserID,
|
UserID: r.UserID,
|
||||||
PayeeName: r.PayeeName,
|
PayeeName: r.PayeeName,
|
||||||
PayeePhone: phone,
|
PayeePhone: phone,
|
||||||
UploaderName: r.UploaderName,
|
UploaderName: r.UploaderName,
|
||||||
SourceChannel: r.SourceChannel,
|
SourceChannel: r.SourceChannel,
|
||||||
AccountType: r.AccountType,
|
AccountType: r.AccountType,
|
||||||
AccountName: r.AccountName,
|
AccountName: r.AccountName,
|
||||||
AccountNo: r.AccountNo,
|
AccountNo: r.AccountNo,
|
||||||
BankName: r.BankName,
|
BankName: r.BankName,
|
||||||
AmountCent: r.AmountCent,
|
AmountCent: r.AmountCent,
|
||||||
FeeCent: r.FeeCent,
|
FeeCent: r.FeeCent,
|
||||||
ActualAmountCent: r.ActualAmountCent,
|
ActualAmountCent: r.ActualAmountCent,
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
RawStatus: r.RawStatus,
|
RawStatus: r.RawStatus,
|
||||||
BusinessCreatedAt: r.BusinessCreatedAt,
|
BusinessCreatedAt: r.BusinessCreatedAt,
|
||||||
PaidAt: r.PaidAt,
|
PaidAt: r.PaidAt,
|
||||||
OperatorID: r.OperatorID,
|
OperatorID: r.OperatorID,
|
||||||
OperatorName: r.OperatorName,
|
OperatorName: r.OperatorName,
|
||||||
Remark: r.Remark,
|
Remark: r.Remark,
|
||||||
Category: r.Category,
|
Category: r.Category,
|
||||||
VoucherURL: r.VoucherURL,
|
CustomCategoryName: r.CustomCategoryName,
|
||||||
VoidedAt: r.VoidedAt,
|
VoucherURL: r.VoucherURL,
|
||||||
VoidedBy: r.VoidedBy,
|
VoidedAt: r.VoidedAt,
|
||||||
VoidedByName: r.VoidedByName,
|
VoidedBy: r.VoidedBy,
|
||||||
VoidReason: r.VoidReason,
|
VoidedByName: r.VoidedByName,
|
||||||
|
VoidReason: r.VoidReason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
`CREATE TABLE users (id INTEGER PRIMARY KEY, nickname TEXT, phone TEXT)`,
|
`CREATE TABLE users (id INTEGER PRIMARY KEY, nickname TEXT, phone TEXT)`,
|
||||||
`CREATE TABLE admin_users (id INTEGER PRIMARY KEY, nickname TEXT, username TEXT)`,
|
`CREATE TABLE admin_users (id INTEGER PRIMARY KEY, nickname TEXT, username TEXT)`,
|
||||||
`CREATE TABLE manual_disbursements (
|
`CREATE TABLE manual_disbursements (
|
||||||
id INTEGER PRIMARY KEY, disbursement_no TEXT, category TEXT, payee_name TEXT,
|
id INTEGER PRIMARY KEY, disbursement_no TEXT, category TEXT, custom_category_name TEXT NOT NULL DEFAULT '', payee_name TEXT,
|
||||||
amount_cent INTEGER, paid_at DATETIME, remark TEXT, voucher_url TEXT,
|
amount_cent INTEGER, paid_at DATETIME, remark TEXT, voucher_url TEXT,
|
||||||
status TEXT, created_by INTEGER, voided_by INTEGER, voided_at DATETIME,
|
status TEXT, created_by INTEGER, voided_by INTEGER, voided_at DATETIME,
|
||||||
void_reason TEXT, created_at DATETIME
|
void_reason TEXT, created_at DATETIME
|
||||||
@@ -87,11 +87,11 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := db.Exec(`INSERT INTO manual_disbursements
|
if err := db.Exec(`INSERT INTO manual_disbursements
|
||||||
(id, disbursement_no, category, payee_name, amount_cent, paid_at, remark,
|
(id, disbursement_no, category, custom_category_name, payee_name, amount_cent, paid_at, remark,
|
||||||
voucher_url, status, created_by, voided_by, voided_at, void_reason, created_at)
|
voucher_url, status, created_by, voided_by, voided_at, void_reason, created_at)
|
||||||
VALUES
|
VALUES
|
||||||
(20, 'OD202607010001', 'user_compensation', '李四', 2500, ?, '用户补偿', '', 'paid', 7, NULL, NULL, '', ?),
|
(20, 'OD202607010001', 'custom', '临时活动支出', '李四', 2500, ?, '用户补偿', '', 'paid', 7, NULL, NULL, '', ?),
|
||||||
(21, 'OD202607010002', 'other', '测试单位', 6000, ?, '重复录入', '', 'voided', 7, 8, ?, '重复记录', ?)`,
|
(21, 'OD202607010002', 'other', '', '测试单位', 6000, ?, '重复录入', '', 'voided', 7, 8, ?, '重复记录', ?)`,
|
||||||
at(6), at(5), at(7), at(8), at(5)).Error; err != nil {
|
at(6), at(5), at(7), at(8), at(5)).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ func TestDisbursementListCombinesPlatformSettlementsAndWithdrawals(t *testing.T)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if manualItem == nil || manualItem.SourceType != "manual_offline" || manualItem.Category != "user_compensation" || manualItem.OperatorName != "财务甲" {
|
if manualItem == nil || manualItem.SourceType != "manual_offline" || manualItem.Category != "custom" || manualItem.CustomCategoryName != "临时活动支出" || manualItem.OperatorName != "财务甲" {
|
||||||
t.Fatalf("其他线下出款信息不正确: %+v", manualItem)
|
t.Fatalf("其他线下出款信息不正确: %+v", manualItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,45 +102,47 @@ type DisbursementListSummaryDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DisbursementItemDTO struct {
|
type DisbursementItemDTO struct {
|
||||||
SourceType string `json:"source_type"`
|
SourceType string `json:"source_type"`
|
||||||
SourceID uint64 `json:"source_id"`
|
SourceID uint64 `json:"source_id"`
|
||||||
BusinessNo string `json:"business_no"`
|
BusinessNo string `json:"business_no"`
|
||||||
OrderID uint64 `json:"order_id,omitempty"`
|
OrderID uint64 `json:"order_id,omitempty"`
|
||||||
WithdrawalID uint64 `json:"withdrawal_id,omitempty"`
|
WithdrawalID uint64 `json:"withdrawal_id,omitempty"`
|
||||||
UserID uint64 `json:"user_id"`
|
UserID uint64 `json:"user_id"`
|
||||||
PayeeName string `json:"payee_name"`
|
PayeeName string `json:"payee_name"`
|
||||||
PayeePhone string `json:"payee_phone"`
|
PayeePhone string `json:"payee_phone"`
|
||||||
UploaderName string `json:"uploader_name"`
|
UploaderName string `json:"uploader_name"`
|
||||||
SourceChannel string `json:"source_channel"`
|
SourceChannel string `json:"source_channel"`
|
||||||
AccountType string `json:"account_type"`
|
AccountType string `json:"account_type"`
|
||||||
AccountName string `json:"account_name"`
|
AccountName string `json:"account_name"`
|
||||||
AccountNo string `json:"account_no"`
|
AccountNo string `json:"account_no"`
|
||||||
BankName string `json:"bank_name"`
|
BankName string `json:"bank_name"`
|
||||||
AmountCent int64 `json:"amount_cent"`
|
AmountCent int64 `json:"amount_cent"`
|
||||||
FeeCent int64 `json:"fee_cent"`
|
FeeCent int64 `json:"fee_cent"`
|
||||||
ActualAmountCent int64 `json:"actual_amount_cent"`
|
ActualAmountCent int64 `json:"actual_amount_cent"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RawStatus string `json:"raw_status"`
|
RawStatus string `json:"raw_status"`
|
||||||
BusinessCreatedAt *time.Time `json:"business_created_at,omitempty"`
|
BusinessCreatedAt *time.Time `json:"business_created_at,omitempty"`
|
||||||
PaidAt *time.Time `json:"paid_at,omitempty"`
|
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||||
OperatorID *uint64 `json:"operator_id,omitempty"`
|
OperatorID *uint64 `json:"operator_id,omitempty"`
|
||||||
OperatorName string `json:"operator_name"`
|
OperatorName string `json:"operator_name"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
VoucherURL string `json:"voucher_url"`
|
CustomCategoryName string `json:"custom_category_name"`
|
||||||
VoidedAt *time.Time `json:"voided_at,omitempty"`
|
VoucherURL string `json:"voucher_url"`
|
||||||
VoidedBy *uint64 `json:"voided_by,omitempty"`
|
VoidedAt *time.Time `json:"voided_at,omitempty"`
|
||||||
VoidedByName string `json:"voided_by_name"`
|
VoidedBy *uint64 `json:"voided_by,omitempty"`
|
||||||
VoidReason string `json:"void_reason"`
|
VoidedByName string `json:"voided_by_name"`
|
||||||
|
VoidReason string `json:"void_reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateManualDisbursementRequest struct {
|
type CreateManualDisbursementRequest struct {
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
PayeeName string `json:"payee_name"`
|
CustomCategoryName string `json:"custom_category_name"`
|
||||||
AmountCent int64 `json:"amount_cent"`
|
PayeeName string `json:"payee_name"`
|
||||||
PaidAt time.Time `json:"paid_at"`
|
AmountCent int64 `json:"amount_cent"`
|
||||||
Remark string `json:"remark"`
|
PaidAt time.Time `json:"paid_at"`
|
||||||
VoucherURL string `json:"voucher_url"`
|
Remark string `json:"remark"`
|
||||||
|
VoucherURL string `json:"voucher_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VoidManualDisbursementRequest struct {
|
type VoidManualDisbursementRequest struct {
|
||||||
@@ -148,22 +150,23 @@ type VoidManualDisbursementRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ManualDisbursementDTO struct {
|
type ManualDisbursementDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
DisbursementNo string `json:"disbursement_no"`
|
DisbursementNo string `json:"disbursement_no"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
PayeeName string `json:"payee_name"`
|
CustomCategoryName string `json:"custom_category_name"`
|
||||||
AmountCent int64 `json:"amount_cent"`
|
PayeeName string `json:"payee_name"`
|
||||||
PaidAt time.Time `json:"paid_at"`
|
AmountCent int64 `json:"amount_cent"`
|
||||||
Remark string `json:"remark"`
|
PaidAt time.Time `json:"paid_at"`
|
||||||
VoucherURL string `json:"voucher_url"`
|
Remark string `json:"remark"`
|
||||||
Status string `json:"status"`
|
VoucherURL string `json:"voucher_url"`
|
||||||
CreatedBy uint64 `json:"created_by"`
|
Status string `json:"status"`
|
||||||
CreatedByName string `json:"created_by_name"`
|
CreatedBy uint64 `json:"created_by"`
|
||||||
VoidedBy *uint64 `json:"voided_by,omitempty"`
|
CreatedByName string `json:"created_by_name"`
|
||||||
VoidedByName string `json:"voided_by_name"`
|
VoidedBy *uint64 `json:"voided_by,omitempty"`
|
||||||
VoidedAt *time.Time `json:"voided_at,omitempty"`
|
VoidedByName string `json:"voided_by_name"`
|
||||||
VoidReason string `json:"void_reason"`
|
VoidedAt *time.Time `json:"voided_at,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
VoidReason string `json:"void_reason"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FinanceSummaryDTO struct {
|
type FinanceSummaryDTO struct {
|
||||||
|
|||||||
@@ -16,21 +16,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type manualDisbursementRecord struct {
|
type manualDisbursementRecord struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
DisbursementNo string
|
DisbursementNo string
|
||||||
Category string
|
Category string
|
||||||
PayeeName string
|
CustomCategoryName string
|
||||||
AmountCent int64
|
PayeeName string
|
||||||
PaidAt time.Time
|
AmountCent int64
|
||||||
Remark string
|
PaidAt time.Time
|
||||||
VoucherURL string
|
Remark string
|
||||||
Status string
|
VoucherURL string
|
||||||
CreatedBy uint64
|
Status string
|
||||||
VoidedBy *uint64
|
CreatedBy uint64
|
||||||
VoidedAt *time.Time
|
VoidedBy *uint64
|
||||||
VoidReason string
|
VoidedAt *time.Time
|
||||||
CreatedAt time.Time
|
VoidReason string
|
||||||
UpdatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manualDisbursementRecord) TableName() string {
|
func (manualDisbursementRecord) TableName() string {
|
||||||
@@ -48,15 +49,16 @@ func (r *Repository) CreateManualDisbursement(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
record := manualDisbursementRecord{
|
record := manualDisbursementRecord{
|
||||||
DisbursementNo: disbursementNo,
|
DisbursementNo: disbursementNo,
|
||||||
Category: req.Category,
|
Category: req.Category,
|
||||||
PayeeName: req.PayeeName,
|
CustomCategoryName: req.CustomCategoryName,
|
||||||
AmountCent: req.AmountCent,
|
PayeeName: req.PayeeName,
|
||||||
PaidAt: req.PaidAt,
|
AmountCent: req.AmountCent,
|
||||||
Remark: req.Remark,
|
PaidAt: req.PaidAt,
|
||||||
VoucherURL: req.VoucherURL,
|
Remark: req.Remark,
|
||||||
Status: "paid",
|
VoucherURL: req.VoucherURL,
|
||||||
CreatedBy: adminID,
|
Status: "paid",
|
||||||
|
CreatedBy: adminID,
|
||||||
}
|
}
|
||||||
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Create(&record).Error; err != nil {
|
if err := tx.Create(&record).Error; err != nil {
|
||||||
@@ -71,12 +73,13 @@ func (r *Repository) CreateManualDisbursement(
|
|||||||
BizID: &id,
|
BizID: &id,
|
||||||
Meta: meta,
|
Meta: meta,
|
||||||
Detail: map[string]any{
|
Detail: map[string]any{
|
||||||
"disbursement_no": disbursementNo,
|
"disbursement_no": disbursementNo,
|
||||||
"category": req.Category,
|
"category": req.Category,
|
||||||
"payee_name": req.PayeeName,
|
"custom_category_name": req.CustomCategoryName,
|
||||||
"amount_cent": req.AmountCent,
|
"payee_name": req.PayeeName,
|
||||||
"paid_at": req.PaidAt,
|
"amount_cent": req.AmountCent,
|
||||||
"has_voucher": req.VoucherURL != "",
|
"paid_at": req.PaidAt,
|
||||||
|
"has_voucher": req.VoucherURL != "",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -153,42 +156,44 @@ func (r *Repository) findManualDisbursement(ctx context.Context, id uint64) (*Ma
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &ManualDisbursementDTO{
|
return &ManualDisbursementDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
DisbursementNo: row.DisbursementNo,
|
DisbursementNo: row.DisbursementNo,
|
||||||
Category: row.Category,
|
Category: row.Category,
|
||||||
PayeeName: row.PayeeName,
|
CustomCategoryName: row.CustomCategoryName,
|
||||||
AmountCent: row.AmountCent,
|
PayeeName: row.PayeeName,
|
||||||
PaidAt: row.PaidAt,
|
AmountCent: row.AmountCent,
|
||||||
Remark: row.Remark,
|
PaidAt: row.PaidAt,
|
||||||
VoucherURL: row.VoucherURL,
|
Remark: row.Remark,
|
||||||
Status: row.Status,
|
VoucherURL: row.VoucherURL,
|
||||||
CreatedBy: row.CreatedBy,
|
Status: row.Status,
|
||||||
CreatedByName: row.CreatedByName,
|
CreatedBy: row.CreatedBy,
|
||||||
VoidedBy: row.VoidedBy,
|
CreatedByName: row.CreatedByName,
|
||||||
VoidedByName: row.VoidedByName,
|
VoidedBy: row.VoidedBy,
|
||||||
VoidedAt: row.VoidedAt,
|
VoidedByName: row.VoidedByName,
|
||||||
VoidReason: row.VoidReason,
|
VoidedAt: row.VoidedAt,
|
||||||
CreatedAt: row.CreatedAt,
|
VoidReason: row.VoidReason,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type manualDisbursementDetailRow struct {
|
type manualDisbursementDetailRow struct {
|
||||||
ID uint64
|
ID uint64
|
||||||
DisbursementNo string
|
DisbursementNo string
|
||||||
Category string
|
Category string
|
||||||
PayeeName string
|
CustomCategoryName string
|
||||||
AmountCent int64
|
PayeeName string
|
||||||
PaidAt time.Time
|
AmountCent int64
|
||||||
Remark string
|
PaidAt time.Time
|
||||||
VoucherURL string
|
Remark string
|
||||||
Status string
|
VoucherURL string
|
||||||
CreatedBy uint64
|
Status string
|
||||||
CreatedByName string
|
CreatedBy uint64
|
||||||
VoidedBy *uint64
|
CreatedByName string
|
||||||
VoidedByName string
|
VoidedBy *uint64
|
||||||
VoidedAt *time.Time
|
VoidedByName string
|
||||||
VoidReason string
|
VoidedAt *time.Time
|
||||||
CreatedAt time.Time
|
VoidReason string
|
||||||
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func newManualDisbursementNo() (string, error) {
|
func newManualDisbursementNo() (string, error) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func TestManualDisbursementCreateAndVoid(t *testing.T) {
|
|||||||
statements := []string{
|
statements := []string{
|
||||||
`CREATE TABLE manual_disbursements (
|
`CREATE TABLE manual_disbursements (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT, disbursement_no TEXT NOT NULL UNIQUE,
|
id INTEGER PRIMARY KEY AUTOINCREMENT, disbursement_no TEXT NOT NULL UNIQUE,
|
||||||
category TEXT NOT NULL, payee_name TEXT NOT NULL, amount_cent INTEGER NOT NULL,
|
category TEXT NOT NULL, custom_category_name TEXT NOT NULL DEFAULT '', payee_name TEXT NOT NULL, amount_cent INTEGER NOT NULL,
|
||||||
paid_at DATETIME NOT NULL, remark TEXT NOT NULL, voucher_url TEXT NOT NULL,
|
paid_at DATETIME NOT NULL, remark TEXT NOT NULL, voucher_url TEXT NOT NULL,
|
||||||
status TEXT NOT NULL, created_by INTEGER NOT NULL, voided_by INTEGER,
|
status TEXT NOT NULL, created_by INTEGER NOT NULL, voided_by INTEGER,
|
||||||
voided_at DATETIME, void_reason TEXT NOT NULL DEFAULT '', created_at DATETIME,
|
voided_at DATETIME, void_reason TEXT NOT NULL DEFAULT '', created_at DATETIME,
|
||||||
@@ -47,17 +47,18 @@ func TestManualDisbursementCreateAndVoid(t *testing.T) {
|
|||||||
repo := NewRepository(db)
|
repo := NewRepository(db)
|
||||||
paidAt := time.Date(2026, 7, 20, 9, 30, 0, 0, timeutil.ShanghaiLocation())
|
paidAt := time.Date(2026, 7, 20, 9, 30, 0, 0, timeutil.ShanghaiLocation())
|
||||||
created, err := repo.CreateManualDisbursement(t.Context(), CreateManualDisbursementRequest{
|
created, err := repo.CreateManualDisbursement(t.Context(), CreateManualDisbursementRequest{
|
||||||
Category: "operating_expense",
|
Category: "custom",
|
||||||
PayeeName: "测试供应商",
|
CustomCategoryName: "临时活动支出",
|
||||||
AmountCent: 8800,
|
PayeeName: "测试供应商",
|
||||||
PaidAt: paidAt,
|
AmountCent: 8800,
|
||||||
Remark: "测试运营支出",
|
PaidAt: paidAt,
|
||||||
VoucherURL: "/api/files/object?key=manual-disbursement%2Fvoucher.webp",
|
Remark: "测试运营支出",
|
||||||
|
VoucherURL: "/api/files/object?key=manual-disbursement%2Fvoucher.webp",
|
||||||
}, 7, auditlog.Meta{RequestID: "req-create"})
|
}, 7, auditlog.Meta{RequestID: "req-create"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("创建其他线下出款失败: %v", err)
|
t.Fatalf("创建其他线下出款失败: %v", err)
|
||||||
}
|
}
|
||||||
if created.ID == 0 || created.DisbursementNo == "" || created.Status != "paid" || created.CreatedByName != "财务甲" {
|
if created.ID == 0 || created.DisbursementNo == "" || created.Status != "paid" || created.CreatedByName != "财务甲" || created.CustomCategoryName != "临时活动支出" {
|
||||||
t.Fatalf("创建结果不正确: %+v", created)
|
t.Fatalf("创建结果不正确: %+v", created)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +83,13 @@ func TestManualDisbursementCreateAndVoid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestManualDisbursementValidation(t *testing.T) {
|
func TestManualDisbursementValidation(t *testing.T) {
|
||||||
|
if !validManualDisbursementCategory("duoduo_deposit_refund") {
|
||||||
|
t.Fatal("多多退押金分类应为有效分类")
|
||||||
|
}
|
||||||
|
if !validManualDisbursementCustomCategoryName("custom", "临时活动支出") || validManualDisbursementCustomCategoryName("custom", "") {
|
||||||
|
t.Fatal("自定义分类名称校验不正确")
|
||||||
|
}
|
||||||
|
|
||||||
service := NewService(&Repository{})
|
service := NewService(&Repository{})
|
||||||
_, err := service.CreateManualDisbursement(t.Context(), CreateManualDisbursementRequest{
|
_, err := service.CreateManualDisbursement(t.Context(), CreateManualDisbursementRequest{
|
||||||
Category: "unknown",
|
Category: "unknown",
|
||||||
|
|||||||
@@ -74,10 +74,12 @@ func (s *Service) CreateManualDisbursement(
|
|||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
req.Category = strings.TrimSpace(req.Category)
|
req.Category = strings.TrimSpace(req.Category)
|
||||||
|
req.CustomCategoryName = strings.TrimSpace(req.CustomCategoryName)
|
||||||
req.PayeeName = strings.TrimSpace(req.PayeeName)
|
req.PayeeName = strings.TrimSpace(req.PayeeName)
|
||||||
req.Remark = strings.TrimSpace(req.Remark)
|
req.Remark = strings.TrimSpace(req.Remark)
|
||||||
req.VoucherURL = strings.TrimSpace(req.VoucherURL)
|
req.VoucherURL = strings.TrimSpace(req.VoucherURL)
|
||||||
if !validManualDisbursementCategory(req.Category) ||
|
if !validManualDisbursementCategory(req.Category) ||
|
||||||
|
!validManualDisbursementCustomCategoryName(req.Category, req.CustomCategoryName) ||
|
||||||
utf8.RuneCountInString(req.PayeeName) < 1 || utf8.RuneCountInString(req.PayeeName) > 100 ||
|
utf8.RuneCountInString(req.PayeeName) < 1 || utf8.RuneCountInString(req.PayeeName) > 100 ||
|
||||||
req.AmountCent <= 0 || req.PaidAt.IsZero() ||
|
req.AmountCent <= 0 || req.PaidAt.IsZero() ||
|
||||||
utf8.RuneCountInString(req.Remark) < 1 || utf8.RuneCountInString(req.Remark) > 500 ||
|
utf8.RuneCountInString(req.Remark) < 1 || utf8.RuneCountInString(req.Remark) > 500 ||
|
||||||
@@ -106,13 +108,20 @@ func (s *Service) VoidManualDisbursement(
|
|||||||
|
|
||||||
func validManualDisbursementCategory(category string) bool {
|
func validManualDisbursementCategory(category string) bool {
|
||||||
switch category {
|
switch category {
|
||||||
case "user_compensation", "seller_supplement", "operating_expense", "channel_fee", "other":
|
case "user_compensation", "seller_supplement", "operating_expense", "channel_fee", "duoduo_deposit_refund", "other", "custom":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validManualDisbursementCustomCategoryName(category, name string) bool {
|
||||||
|
if category != "custom" {
|
||||||
|
return name == ""
|
||||||
|
}
|
||||||
|
return utf8.RuneCountInString(name) >= 1 && utf8.RuneCountInString(name) <= 50
|
||||||
|
}
|
||||||
|
|
||||||
func validManualVoucherURL(raw string) bool {
|
func validManualVoucherURL(raw string) bool {
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
ALTER TABLE manual_disbursements
|
||||||
|
ADD COLUMN custom_category_name VARCHAR(50) NOT NULL DEFAULT '' COMMENT '自定义出款分类名称';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
ALTER TABLE manual_disbursements
|
||||||
|
DROP COLUMN custom_category_name;
|
||||||
@@ -144,6 +144,7 @@ export interface FinanceDisbursementItem {
|
|||||||
operator_name: string
|
operator_name: string
|
||||||
remark: string
|
remark: string
|
||||||
category: string
|
category: string
|
||||||
|
custom_category_name: string
|
||||||
voucher_url: string
|
voucher_url: string
|
||||||
voided_at?: string
|
voided_at?: string
|
||||||
voided_by?: number
|
voided_by?: number
|
||||||
@@ -153,6 +154,7 @@ export interface FinanceDisbursementItem {
|
|||||||
|
|
||||||
export interface CreateManualDisbursementPayload {
|
export interface CreateManualDisbursementPayload {
|
||||||
category: string
|
category: string
|
||||||
|
custom_category_name?: string
|
||||||
payee_name: string
|
payee_name: string
|
||||||
amount_cent: number
|
amount_cent: number
|
||||||
paid_at: string
|
paid_at: string
|
||||||
@@ -164,6 +166,7 @@ export interface ManualDisbursement {
|
|||||||
id: number
|
id: number
|
||||||
disbursement_no: string
|
disbursement_no: string
|
||||||
category: string
|
category: string
|
||||||
|
custom_category_name: string
|
||||||
payee_name: string
|
payee_name: string
|
||||||
amount_cent: number
|
amount_cent: number
|
||||||
paid_at: string
|
paid_at: string
|
||||||
|
|||||||
@@ -42,10 +42,13 @@ const manualCategories = [
|
|||||||
{ value: 'seller_supplement', label: '卖家补款' },
|
{ value: 'seller_supplement', label: '卖家补款' },
|
||||||
{ value: 'operating_expense', label: '运营支出' },
|
{ value: 'operating_expense', label: '运营支出' },
|
||||||
{ value: 'channel_fee', label: '渠道费用' },
|
{ value: 'channel_fee', label: '渠道费用' },
|
||||||
|
{ value: 'duoduo_deposit_refund', label: '多多退押金' },
|
||||||
{ value: 'other', label: '其他' },
|
{ value: 'other', label: '其他' },
|
||||||
|
{ value: 'custom', label: '自定义分类' },
|
||||||
]
|
]
|
||||||
const createForm = reactive({
|
const createForm = reactive({
|
||||||
category: '',
|
category: '',
|
||||||
|
custom_category_name: '',
|
||||||
payee_name: '',
|
payee_name: '',
|
||||||
amount_yuan: 0,
|
amount_yuan: 0,
|
||||||
paid_at: new Date(),
|
paid_at: new Date(),
|
||||||
@@ -130,6 +133,7 @@ function openCreate() {
|
|||||||
|
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
createForm.category = ''
|
createForm.category = ''
|
||||||
|
createForm.custom_category_name = ''
|
||||||
createForm.payee_name = ''
|
createForm.payee_name = ''
|
||||||
createForm.amount_yuan = 0
|
createForm.amount_yuan = 0
|
||||||
createForm.paid_at = new Date()
|
createForm.paid_at = new Date()
|
||||||
@@ -169,10 +173,16 @@ async function submitManualDisbursement() {
|
|||||||
ElMessage.warning('请填写出款用途')
|
ElMessage.warning('请填写出款用途')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (createForm.category === 'custom' && !createForm.custom_category_name.trim()) {
|
||||||
|
ElMessage.warning('请填写自定义分类名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
createSaving.value = true
|
createSaving.value = true
|
||||||
try {
|
try {
|
||||||
await createManualDisbursement({
|
await createManualDisbursement({
|
||||||
category: createForm.category,
|
category: createForm.category,
|
||||||
|
custom_category_name:
|
||||||
|
createForm.category === 'custom' ? createForm.custom_category_name.trim() : undefined,
|
||||||
payee_name: createForm.payee_name.trim(),
|
payee_name: createForm.payee_name.trim(),
|
||||||
amount_cent: amountCent,
|
amount_cent: amountCent,
|
||||||
paid_at: createForm.paid_at.toISOString(),
|
paid_at: createForm.paid_at.toISOString(),
|
||||||
@@ -283,7 +293,8 @@ function itemStatusLabel(row: FinanceDisbursementItem) {
|
|||||||
return statusLabel(row.status)
|
return statusLabel(row.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
function categoryLabel(category: string) {
|
function categoryLabel(category: string, customCategoryName = '') {
|
||||||
|
if (category === 'custom' && customCategoryName) return customCategoryName
|
||||||
return manualCategories.find(item => item.value === category)?.label || category || '-'
|
return manualCategories.find(item => item.value === category)?.label || category || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,7 +440,7 @@ function accountTypeLabel(type: string) {
|
|||||||
<small v-if="row.payee_phone">{{ row.payee_phone }}</small>
|
<small v-if="row.payee_phone">{{ row.payee_phone }}</small>
|
||||||
<small v-else-if="row.source_type === 'withdrawal'">用户 ID {{ row.user_id }}</small>
|
<small v-else-if="row.source_type === 'withdrawal'">用户 ID {{ row.user_id }}</small>
|
||||||
<small v-else-if="row.source_type === 'manual_offline'">
|
<small v-else-if="row.source_type === 'manual_offline'">
|
||||||
{{ categoryLabel(row.category) }}
|
{{ categoryLabel(row.category, row.custom_category_name) }}
|
||||||
</small>
|
</small>
|
||||||
<small v-else>联系方式未登记</small>
|
<small v-else>联系方式未登记</small>
|
||||||
<small v-if="row.uploader_name">上传:{{ row.uploader_name }}</small>
|
<small v-if="row.uploader_name">上传:{{ row.uploader_name }}</small>
|
||||||
@@ -445,7 +456,9 @@ function accountTypeLabel(type: string) {
|
|||||||
<small v-if="row.bank_name">{{ row.bank_name }}</small>
|
<small v-if="row.bank_name">{{ row.bank_name }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="row.source_type === 'manual_offline'" class="stack-cell">
|
<div v-else-if="row.source_type === 'manual_offline'" class="stack-cell">
|
||||||
<el-tag size="small" effect="plain">{{ categoryLabel(row.category) }}</el-tag>
|
<el-tag size="small" effect="plain">
|
||||||
|
{{ categoryLabel(row.category, row.custom_category_name) }}
|
||||||
|
</el-tag>
|
||||||
<small>{{ row.voucher_url ? '已上传付款凭证' : '未上传付款凭证' }}</small>
|
<small>{{ row.voucher_url ? '已上传付款凭证' : '未上传付款凭证' }}</small>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="muted-text">线下约定</span>
|
<span v-else class="muted-text">线下约定</span>
|
||||||
@@ -545,6 +558,14 @@ function accountTypeLabel(type: string) {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item v-if="createForm.category === 'custom'" label="自定义分类名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="createForm.custom_category_name"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分类名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="收款人" required>
|
<el-form-item label="收款人" required>
|
||||||
<el-input v-model="createForm.payee_name" maxlength="100" placeholder="姓名或单位名称" />
|
<el-input v-model="createForm.payee_name" maxlength="100" placeholder="姓名或单位名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -634,7 +655,7 @@ function accountTypeLabel(type: string) {
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="出款分类">
|
<el-descriptions-item label="出款分类">
|
||||||
{{ categoryLabel(selectedManual.category) }}
|
{{ categoryLabel(selectedManual.category, selectedManual.custom_category_name) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="收款人">{{ selectedManual.payee_name }}</el-descriptions-item>
|
<el-descriptions-item label="收款人">{{ selectedManual.payee_name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="出款金额">
|
<el-descriptions-item label="出款金额">
|
||||||
|
|||||||
Reference in New Issue
Block a user