第 5 阶段:纠纷、通知与后台-1
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package systemconfig
|
||||
|
||||
import "time"
|
||||
|
||||
type ConfigDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Description string `json:"description"`
|
||||
UpdatedBy *uint64 `json:"updated_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UpdateRequest struct {
|
||||
Value string `json:"value" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package systemconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
items, err := h.service.List()
|
||||
if err != nil {
|
||||
writeConfigError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
key := c.Param("key")
|
||||
var req UpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "配置值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.Update(userID, key, req, AuditMeta{
|
||||
IP: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
})
|
||||
if err != nil {
|
||||
writeConfigError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
value, ok := c.Get(middleware.ContextUserID)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
userID, ok := value.(uint64)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
func writeConfigError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, ErrDependencyUnavailable):
|
||||
response.ServiceUnavailable(c, "数据库未连接")
|
||||
case errors.Is(err, ErrInvalidConfig):
|
||||
response.BadRequest(c, "配置不符合规则")
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, "internal_error", "系统配置服务暂时不可用")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package systemconfig
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type AuditMeta struct {
|
||||
IP string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
type defaultConfig struct {
|
||||
Key string
|
||||
Value string
|
||||
Description string
|
||||
}
|
||||
|
||||
var defaultConfigs = []defaultConfig{
|
||||
{Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"},
|
||||
{Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"},
|
||||
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"},
|
||||
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "租期到期后归还宽限分钟数"},
|
||||
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
|
||||
{Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"},
|
||||
{Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"},
|
||||
{Key: "realname.required_for_order", Value: "false", Description: "下单是否必须完成实名认证"},
|
||||
{Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"},
|
||||
{Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"},
|
||||
{Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"},
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) List() ([]ConfigDTO, error) {
|
||||
if err := r.ensureDefaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []model.SystemConfig
|
||||
if err := r.db.Order("`key` ASC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ConfigDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, toDTO(row))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||
var row model.SystemConfig
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("`key` = ?", key).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
row = model.SystemConfig{
|
||||
Key: key,
|
||||
Value: req.Value,
|
||||
Description: req.Description,
|
||||
UpdatedBy: &actorID,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
before := row.Value
|
||||
row.Value = req.Value
|
||||
if req.Description != "" {
|
||||
row.Description = req.Description
|
||||
}
|
||||
row.UpdatedBy = &actorID
|
||||
if err := tx.Save(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, actorID, "system_config.update", row.ID, meta, map[string]any{
|
||||
"key": row.Key,
|
||||
"before": before,
|
||||
"after": row.Value,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return appendAuditLog(tx, actorID, "system_config.create", row.ID, meta, map[string]any{
|
||||
"key": row.Key,
|
||||
"value": row.Value,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toDTO(row)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ensureDefaults() error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range defaultConfigs {
|
||||
row := model.SystemConfig{
|
||||
Key: item.Key,
|
||||
Value: item.Value,
|
||||
Description: item.Description,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
raw, err := json.Marshal(detail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := model.AuditLog{
|
||||
ActorType: "admin",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
BizType: "system_config",
|
||||
BizID: &bizID,
|
||||
IP: meta.IP,
|
||||
UserAgent: meta.UserAgent,
|
||||
Detail: datatypes.JSON(raw),
|
||||
}
|
||||
return tx.Create(&row).Error
|
||||
}
|
||||
|
||||
func toDTO(row model.SystemConfig) ConfigDTO {
|
||||
return ConfigDTO{
|
||||
ID: row.ID,
|
||||
Key: row.Key,
|
||||
Value: row.Value,
|
||||
Description: row.Description,
|
||||
UpdatedBy: row.UpdatedBy,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package systemconfig
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrInvalidConfig = errors.New("invalid config")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) List() ([]ConfigDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.List()
|
||||
}
|
||||
|
||||
func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if key == "" || req.Value == "" {
|
||||
return nil, ErrInvalidConfig
|
||||
}
|
||||
return s.repo.Update(actorID, key, req, meta)
|
||||
}
|
||||
Reference in New Issue
Block a user