Files
junhong_cmp_fiber/internal/domain/employeecollection/application.go
break ce24d5612e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
feat(员工代收款): 新增员工代收款账单闭环
- 新增 6 张表与成对迁移 000212,扩展企业微信审批场景业务类型白名单
- 后台线下套餐订单与两条代理线下充值入账路径在来源成功事务内建账,来源唯一键幂等
- 核销申请、审批尝试记录、账单分摊预占与驳回重提,审批业务类型 employee_collection_approval
- 企业微信终态消费幂等:通过转已核销、驳回释放预占、通过后撤销不回滚并转异常终态
- 退款成功事务内按 bill_id+refund_id 幂等冲销账单或仅写退款关联提示
- 线下收款方式字典、账单查询/统计/关闭、申请查询与代办权限,均写入事务内审计

OpenSpec Change: add-employee-collection-bills
2026-09-10 18:24:05 +08:00

111 lines
4.6 KiB
Go

package employeecollection
import (
"strings"
"time"
"unicode/utf8"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// ApplicationInput 是核销申请提交的领域输入;账单分摊由 ValidateAllocations 单独校验。
type ApplicationInput struct {
// PaidAmount 表示人工确认的付款金额(分)。
PaidAmount int64
// PayerName 表示付款方名称。
PayerName string
// PaidAt 表示付款时间。
PaidAt time.Time
// ExternalTransactionNo 表示人工确认的外部交易流水号。
ExternalTransactionNo string
// Remark 表示申请备注。
Remark string
// ActingReason 表示代办原因,仅代办提交时必填。
ActingReason string
// PaymentVoucherKeys 表示支付凭证对象存储键列表。
PaymentVoucherKeys []string
}
// NormalizedApplicationInput 是通过校验并去空格后的核销申请事实。
type NormalizedApplicationInput struct {
PaidAmount int64
PayerName string
PaidAt time.Time
ExternalTransactionNo string
Remark string
ActingReason string
PaymentVoucherKeys []string
}
// NormalizeApplicationInput 校验并规范化核销申请输入。
// acting 表示本次是否由超级管理员为他人代办:代办必须填写原因,本人办理不得填写原因。
func NormalizeApplicationInput(input ApplicationInput, acting bool) (NormalizedApplicationInput, error) {
if input.PaidAmount <= 0 {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "付款金额必须大于零")
}
if input.PaidAt.IsZero() {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "付款时间必填")
}
payerName := strings.TrimSpace(input.PayerName)
if payerName == "" {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "付款方名称必填")
}
if utf8.RuneCountInString(payerName) > constants.EmployeeCollectionPayerNameMaxLength {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "付款方名称长度超出限制")
}
externalTransactionNo := strings.TrimSpace(input.ExternalTransactionNo)
if externalTransactionNo == "" {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "外部交易流水号必填")
}
if utf8.RuneCountInString(externalTransactionNo) > constants.EmployeeCollectionExternalTransactionNoMaxLength {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "外部交易流水号长度超出限制")
}
remark := strings.TrimSpace(input.Remark)
if utf8.RuneCountInString(remark) > constants.EmployeeCollectionRemarkMaxLength {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "核销申请备注长度超出限制")
}
actingReason := strings.TrimSpace(input.ActingReason)
if utf8.RuneCountInString(actingReason) > constants.EmployeeCollectionRemarkMaxLength {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "代办原因长度超出限制")
}
if acting && actingReason == "" {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "超级管理员代办核销申请必须填写代办原因")
}
if !acting && actingReason != "" {
return NormalizedApplicationInput{}, errors.New(errors.CodeInvalidParam, "本人办理核销申请不能填写代办原因")
}
vouchers, err := NormalizePaymentVouchers(input.PaymentVoucherKeys)
if err != nil {
return NormalizedApplicationInput{}, err
}
return NormalizedApplicationInput{
PaidAmount: input.PaidAmount, PayerName: payerName,
PaidAt: input.PaidAt.UTC(), ExternalTransactionNo: externalTransactionNo,
Remark: remark, ActingReason: actingReason, PaymentVoucherKeys: vouchers,
}, nil
}
// ValidateApplicationResubmit 校验申请当前状态允许修改并重提。
// 只有企业微信最终驳回的申请可以修改重提;已通过、审批中与异常终态一律拒绝。
func ValidateApplicationResubmit(status int) error {
if status == constants.EmployeeCollectionApplicationStatusRejected {
return nil
}
return errors.New(errors.CodeEmployeeCollectionApplicationStatusInvalid)
}
// MaskExternalTransactionNo 生成外部交易流水号的脱敏展示,用于审计与日志,不保留完整流水。
// 长度不超过 8 时整体掩码,否则保留首尾各 4 位。
func MaskExternalTransactionNo(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
runes := []rune(trimmed)
if len(runes) <= 8 {
return "****"
}
return string(runes[:4]) + "****" + string(runes[len(runes)-4:])
}