feat: 完善平台总后台模块
This commit is contained in:
@@ -23,12 +23,11 @@ type LoginRequest struct {
|
||||
|
||||
// LoginReply 是后台登录成功后的访问凭证与账号状态。
|
||||
type LoginReply struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
Identity string `json:"identity"`
|
||||
DisplayName string `json:"display_name"`
|
||||
RoleCode string `json:"role_code"`
|
||||
MustChangePassword bool `json:"must_change_password"`
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
Identity string `json:"identity"`
|
||||
DisplayName string `json:"display_name"`
|
||||
RoleCode string `json:"role_code"`
|
||||
}
|
||||
|
||||
// Login 校验平台账号密码并签发 BSM JWT。
|
||||
@@ -39,7 +38,7 @@ func Login(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var account models.IdnAccount
|
||||
var account models.PlatfromAccount
|
||||
err := impl.DBService.Where("username = ?", strings.TrimSpace(request.Username)).First(&account).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -60,23 +59,22 @@ func Login(ctx *gin.Context) {
|
||||
|
||||
accessToken, err := token.New(env.Runtime.JwtSecretKey).GenerateJwt(
|
||||
0,
|
||||
account.Identity.String(),
|
||||
account.Identity,
|
||||
"platform_admin",
|
||||
account.RoleCode,
|
||||
account.PlatformRoleCode,
|
||||
map[string]string{"username": account.Username, "display_name": account.DisplayName},
|
||||
map[string]string{"must_change_password": boolText(account.MustChangePassword)},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, LoginReply{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "JWT",
|
||||
Identity: account.Identity.String(),
|
||||
DisplayName: account.DisplayName,
|
||||
RoleCode: account.RoleCode,
|
||||
MustChangePassword: account.MustChangePassword,
|
||||
AccessToken: accessToken,
|
||||
TokenType: "JWT",
|
||||
Identity: account.Identity,
|
||||
DisplayName: account.DisplayName,
|
||||
RoleCode: account.PlatformRoleCode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,14 +85,14 @@ func CurrentProfile(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
var account models.IdnAccount
|
||||
var account models.PlatfromAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"identity": account.Identity.String(), "username": account.Username, "display_name": account.DisplayName,
|
||||
"role_code": account.RoleCode, "must_change_password": account.MustChangePassword, "mfa_enabled": account.MFAEnabled,
|
||||
"identity": account.Identity, "username": account.Username, "display_name": account.DisplayName,
|
||||
"avatar": account.Avatar, "role_code": account.PlatformRoleCode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -116,7 +114,7 @@ func ChangePassword(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.IdnAccount
|
||||
var account models.PlatfromAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
@@ -130,17 +128,9 @@ func ChangePassword(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&account).Updates(map[string]any{"password_hash": string(passwordHash), "must_change_password": false}).Error; err != nil {
|
||||
if err := impl.DBService.Model(&account).Update("password_hash", string(passwordHash)).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
// boolText 将布尔值转换为 JWT 扩展字段约定的字符串。
|
||||
func boolText(value bool) string {
|
||||
if value {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/utils"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PingHello 返回匿名健康状态。
|
||||
@@ -14,7 +18,7 @@ func PingHello(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, gin.H{"service": "platform-api", "status": "ok"})
|
||||
}
|
||||
|
||||
// DashboardOverview 返回组织与安全运营的首期概览数据。
|
||||
// DashboardOverview 返回平台总后台的运营概览数据。
|
||||
func DashboardOverview(ctx *gin.Context) {
|
||||
overview, err := models.GetDashboardOverview()
|
||||
if err != nil {
|
||||
@@ -24,131 +28,344 @@ func DashboardOverview(ctx *gin.Context) {
|
||||
infra.Response.Success(ctx, overview)
|
||||
}
|
||||
|
||||
// CreateOrgGasStationRequest 是创建 org_gas_station 的请求体。
|
||||
type CreateOrgGasStationRequest struct {
|
||||
StationCode string `json:"station_code" binding:"required,max=32"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
Principal string `json:"principal" binding:"required,max=64"`
|
||||
ServiceArea string `json:"service_area" binding:"required,max=128"`
|
||||
// ListGasBasic 查询可燃气体站分页列表。
|
||||
func ListGasBasic(ctx *gin.Context) { listPage[models.GasBasic](ctx) }
|
||||
|
||||
// GetGasBasic 查询一个可燃气体站。
|
||||
func GetGasBasic(ctx *gin.Context) { getByIdentity[models.GasBasic](ctx) }
|
||||
|
||||
// CreateGasBasic 创建可燃气体站档案。
|
||||
func CreateGasBasic(ctx *gin.Context) {
|
||||
var request models.GasBasic
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Code == "" || request.Name == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("draft")
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
}
|
||||
|
||||
// OrgGasStationListReply 是 org_gas_station 的标准分页响应。
|
||||
type OrgGasStationListReply struct {
|
||||
Total int64 `json:"total"`
|
||||
List []models.OrgGasStation `json:"list"`
|
||||
}
|
||||
|
||||
// CreateOrgGasStation 创建待审核气站并由数据库层保证唯一编码。
|
||||
func CreateOrgGasStation(ctx *gin.Context) {
|
||||
var request CreateOrgGasStationRequest
|
||||
// UpdateGasBasic 更新可燃气体站基础资料。
|
||||
func UpdateGasBasic(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
CreditCode string `json:"credit_code" binding:"max=64"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
Longitude string `json:"longitude" binding:"max=32"`
|
||||
Latitude string `json:"latitude" binding:"max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
data := models.OrgGasStation{StationCode: request.StationCode, Name: request.Name, Principal: request.Principal, ServiceArea: request.ServiceArea}
|
||||
data.Identity = models.NewIdentity()
|
||||
data.Status = "draft"
|
||||
data.Version = 1
|
||||
if err := models.CreateOrgGasStation(&data); err != nil {
|
||||
updateByIdentity(ctx, &models.GasBasic{}, gin.H{"name": request.Name, "credit_code": request.CreditCode, "principal": request.Principal, "address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude})
|
||||
}
|
||||
|
||||
// ListDeliveryBasic 查询配送点分页列表。
|
||||
func ListDeliveryBasic(ctx *gin.Context) { listPage[models.DeliveryBasic](ctx) }
|
||||
|
||||
// GetDeliveryBasic 查询一个配送点。
|
||||
func GetDeliveryBasic(ctx *gin.Context) { getByIdentity[models.DeliveryBasic](ctx) }
|
||||
|
||||
// CreateDeliveryBasic 创建配送点档案。
|
||||
func CreateDeliveryBasic(ctx *gin.Context) {
|
||||
var request models.DeliveryBasic
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryCode == "" || request.Name == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("draft")
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
}
|
||||
|
||||
// UpdateDeliveryBasic 更新配送点基础资料。
|
||||
func UpdateDeliveryBasic(ctx *gin.Context) {
|
||||
var request struct {
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
Name string `json:"name" binding:"required,max=128"`
|
||||
Principal string `json:"principal" binding:"max=64"`
|
||||
Address string `json:"address" binding:"max=255"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateByIdentity(ctx, &models.DeliveryBasic{}, gin.H{"gas_basic_id": request.GasBasicID, "name": request.Name, "principal": request.Principal, "address": request.Address})
|
||||
}
|
||||
|
||||
// ListStaff 查询服务人员分页列表。
|
||||
func ListStaff(ctx *gin.Context) { listPage[models.StaffAccount](ctx) }
|
||||
|
||||
// GetStaff 查询一个服务人员档案。
|
||||
func GetStaff(ctx *gin.Context) { getByIdentity[models.StaffAccount](ctx) }
|
||||
|
||||
// CreateStaff 创建服务人员档案。
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
var request models.StaffAccount
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Name == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("draft")
|
||||
if request.WorkStatus == "" {
|
||||
request.WorkStatus = "off_duty"
|
||||
}
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
}
|
||||
|
||||
// UpdateStaff 更新服务人员档案。
|
||||
func UpdateStaff(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"max=64"`
|
||||
GasBasicID uint64 `json:"gas_basic_id"`
|
||||
DeliveryBasicID uint64 `json:"delivery_basic_id"`
|
||||
WorkStatus string `json:"work_status" binding:"max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateByIdentity(ctx, &models.StaffAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode, "gas_basic_id": request.GasBasicID, "delivery_basic_id": request.DeliveryBasicID, "work_status": request.WorkStatus})
|
||||
}
|
||||
|
||||
// ListUser 查询业主客户分页列表。
|
||||
func ListUser(ctx *gin.Context) { listPage[models.UserAccount](ctx) }
|
||||
|
||||
// GetUser 查询一个业主客户档案。
|
||||
func GetUser(ctx *gin.Context) { getByIdentity[models.UserAccount](ctx) }
|
||||
|
||||
// CreateUser 创建业主客户档案。
|
||||
func CreateUser(ctx *gin.Context) {
|
||||
var request models.UserAccount
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Name == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("enabled")
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
}
|
||||
|
||||
// UpdateUser 更新业主客户档案。
|
||||
func UpdateUser(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
Phone string `json:"phone" binding:"max=32"`
|
||||
Avatar string `json:"avatar" binding:"max=512"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateByIdentity(ctx, &models.UserAccount{}, gin.H{"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName})
|
||||
}
|
||||
|
||||
// ListPlatformRole 查询平台角色分页列表。
|
||||
func ListPlatformRole(ctx *gin.Context) { listPage[models.PlatformRole](ctx) }
|
||||
|
||||
// GetPlatformRole 查询一个平台角色。
|
||||
func GetPlatformRole(ctx *gin.Context) { getByIdentity[models.PlatformRole](ctx) }
|
||||
|
||||
// CreatePlatformRole 创建非内置平台角色。
|
||||
func CreatePlatformRole(ctx *gin.Context) {
|
||||
var request models.PlatformRole
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.RoleCode == "" || request.Name == "" || request.RoleCode == "root" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
request.Entity = newEntity("enabled")
|
||||
request.IsSystem = false
|
||||
if request.DataScope == "" {
|
||||
request.DataScope = "global"
|
||||
}
|
||||
if err := impl.DBService.Create(&request).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, request)
|
||||
}
|
||||
|
||||
// UpdatePlatformRole 更新非内置平台角色。
|
||||
func UpdatePlatformRole(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Name string `json:"name" binding:"required,max=64"`
|
||||
DataScope string `json:"data_scope" binding:"required,max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&role).Updates(gin.H{"name": request.Name, "data_scope": request.DataScope}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, role)
|
||||
}
|
||||
|
||||
// UpdatePlatformRoleStatus 更新非内置平台角色状态,root 等系统角色始终受保护。
|
||||
func UpdatePlatformRoleStatus(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Status string `json:"status" binding:"required,max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&role).Update("status", request.Status).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
// ArchivePlatformRole 归档非内置平台角色,root 等系统角色始终受保护。
|
||||
func ArchivePlatformRole(ctx *gin.Context) {
|
||||
var role models.PlatformRole
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&role).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if role.IsSystem {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&role).Update("status", "archived").Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
// ListPlatformMenu 返回菜单树构建所需的有序菜单列表。
|
||||
func ListPlatformMenu(ctx *gin.Context) {
|
||||
var list []models.PlatformMenu
|
||||
if err := impl.DBService.Order("sort_no asc, id asc").Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": len(list), "list": list})
|
||||
}
|
||||
|
||||
// UpdateRecordStatus 更新主表状态,停用和归档均保留历史记录。
|
||||
func UpdateRecordStatus(ctx *gin.Context, model any) {
|
||||
var request struct {
|
||||
Status string `json:"status" binding:"required,max=32"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateByIdentity(ctx, model, gin.H{"status": request.Status})
|
||||
}
|
||||
|
||||
// ArchiveRecord 通过 archived 状态实现逻辑删除,不物理删除主数据。
|
||||
func ArchiveRecord(ctx *gin.Context, model any) {
|
||||
updateByIdentity(ctx, model, gin.H{"status": "archived"})
|
||||
}
|
||||
|
||||
// ListPlatfromAccount 查询平台账号列表,手机号在展示层脱敏。
|
||||
func ListPlatfromAccount(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListPlatfromAccount(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
views := make([]gin.H, 0, len(list))
|
||||
for _, item := range list {
|
||||
views = append(views, gin.H{"identity": item.Identity, "username": item.Username, "display_name": item.DisplayName, "avatar": item.Avatar, "phone_masked": maskPhone(item.Phone), "platform_role_code": item.PlatformRoleCode, "status": item.Status})
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": views})
|
||||
}
|
||||
|
||||
func newEntity(status string) models.Entity {
|
||||
return models.Entity{Identity: models.NewIdentity(), Status: status, Version: 1}
|
||||
}
|
||||
|
||||
func listPage[T any](ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
var list []T
|
||||
var total int64
|
||||
databaseQuery := impl.DBService.Model(new(T))
|
||||
if err := databaseQuery.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := databaseQuery.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": list})
|
||||
}
|
||||
|
||||
func getByIdentity[T any](ctx *gin.Context) {
|
||||
var data T
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ListOrgGasStation 查询气站分页列表。
|
||||
func ListOrgGasStation(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListOrgGasStation(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
func updateByIdentity(ctx *gin.Context, model any, values map[string]any) {
|
||||
result := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Updates(values)
|
||||
if result.Error != nil {
|
||||
infra.Response.Error(ctx, result.Error)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, OrgGasStationListReply{Total: total, List: list})
|
||||
}
|
||||
|
||||
// OrgDeliveryPointListReply 是 org_delivery_point 的标准分页响应。
|
||||
type OrgDeliveryPointListReply struct {
|
||||
Total int64 `json:"total"`
|
||||
List []models.OrgDeliveryPoint `json:"list"`
|
||||
}
|
||||
|
||||
// ListOrgDeliveryPoint 查询配送点分页列表。
|
||||
func ListOrgDeliveryPoint(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListOrgDeliveryPoint(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
if result.RowsAffected == 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, OrgDeliveryPointListReply{Total: total, List: list})
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
// OrgServicePersonListReply 是 org_service_person 的标准分页响应。
|
||||
type OrgServicePersonListReply struct {
|
||||
Total int64 `json:"total"`
|
||||
List []models.OrgServicePerson `json:"list"`
|
||||
}
|
||||
|
||||
// ListOrgServicePerson 查询服务人员分页列表。
|
||||
func ListOrgServicePerson(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListOrgServicePerson(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
func respondRecordError(ctx *gin.Context, err error) {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, OrgServicePersonListReply{Total: total, List: list})
|
||||
infra.Response.Error(ctx, err)
|
||||
}
|
||||
|
||||
// IdnAccountView 是 idn_account 的最小必要输出,手机号始终脱敏。
|
||||
type IdnAccountView struct {
|
||||
Identity string `json:"identity"`
|
||||
PhoneMasked string `json:"phone_masked"`
|
||||
AccountType string `json:"account_type"`
|
||||
Status string `json:"status"`
|
||||
ServiceArea string `json:"service_area"`
|
||||
}
|
||||
|
||||
// IdnAccountListReply 是 idn_account 的标准分页响应。
|
||||
type IdnAccountListReply struct {
|
||||
Total int64 `json:"total"`
|
||||
List []IdnAccountView `json:"list"`
|
||||
}
|
||||
|
||||
// ListIdnAccount 查询普通用户账户列表。
|
||||
func ListIdnAccount(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListIdnAccount(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
views := make([]IdnAccountView, 0, len(list))
|
||||
for _, item := range list {
|
||||
views = append(views, IdnAccountView{Identity: item.Identity.String(), PhoneMasked: maskPhone(item.Phone), AccountType: item.AccountType, Status: item.Status, ServiceArea: item.ServiceArea})
|
||||
}
|
||||
infra.Response.Success(ctx, IdnAccountListReply{Total: total, List: views})
|
||||
}
|
||||
|
||||
// SafEventListReply 是 saf_event 的标准分页响应。
|
||||
type SafEventListReply struct {
|
||||
Total int64 `json:"total"`
|
||||
List []models.SafEvent `json:"list"`
|
||||
}
|
||||
|
||||
// ListSafEvent 查询安全事件列表。
|
||||
func ListSafEvent(ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
list, total, err := models.ListSafEvent(page, size)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, SafEventListReply{Total: total, List: list})
|
||||
}
|
||||
|
||||
// pageSize 统一约束分页参数,避免各接口出现不同边界。
|
||||
func pageSize(ctx *gin.Context) (int, int) {
|
||||
page := utils.String2Int(ctx.DefaultQuery("page", "1"))
|
||||
size := utils.String2Int(ctx.DefaultQuery("size", "20"))
|
||||
@@ -161,7 +378,6 @@ func pageSize(ctx *gin.Context) (int, int) {
|
||||
return page, size
|
||||
}
|
||||
|
||||
// maskPhone 遵循敏感数据最小展示原则。
|
||||
func maskPhone(phone string) string {
|
||||
if len(phone) < 7 {
|
||||
return "***"
|
||||
|
||||
87
backend/api/internal/logic/upload/upload.go
Normal file
87
backend/api/internal/logic/upload/upload.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Package upload 提供平台总后台的受控文件上传服务。
|
||||
package upload
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const maxUploadSize int64 = 10 << 20
|
||||
|
||||
var allowedExtensions = map[string]struct{}{
|
||||
".jpg": {}, ".jpeg": {}, ".png": {}, ".webp": {}, ".pdf": {},
|
||||
}
|
||||
|
||||
// UploadFileReply 是文件上传完成后返回的受控资源标识。
|
||||
type UploadFileReply struct {
|
||||
URI string `json:"uri"` // 资源访问标识,后续可由对象存储适配层解析
|
||||
OriginalName string `json:"original_name"` // 原始文件名,仅用于展示
|
||||
ContentType string `json:"content_type"` // 客户端声明的媒体类型
|
||||
Size int64 `json:"size"` // 文件字节数
|
||||
}
|
||||
|
||||
// UploadFile 将允许类型的文件保存至本地 Mock 存储,不直接暴露绝对磁盘路径。
|
||||
func UploadFile(ctx *gin.Context) {
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxUploadSize)
|
||||
fileHeader, err := ctx.FormFile("file")
|
||||
if err != nil || fileHeader == nil || fileHeader.Size <= 0 || fileHeader.Size > maxUploadSize {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
extension := strings.ToLower(filepath.Ext(fileHeader.Filename))
|
||||
if _, allowed := allowedExtensions[extension]; !allowed {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
datePath := time.Now().Format("2006/01/02")
|
||||
filename := models.NewIdentity() + extension
|
||||
directory := filepath.Join(uploadRoot(), filepath.FromSlash(datePath))
|
||||
if err := os.MkdirAll(directory, 0o750); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
target, err := os.OpenFile(filepath.Join(directory, filename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o640)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
defer target.Close()
|
||||
if _, err := io.Copy(target, file); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
infra.Response.Success(ctx, UploadFileReply{
|
||||
URI: "/uploads/" + datePath + "/" + filename,
|
||||
OriginalName: fileHeader.Filename,
|
||||
ContentType: fileHeader.Header.Get("Content-Type"),
|
||||
Size: fileHeader.Size,
|
||||
})
|
||||
}
|
||||
|
||||
// uploadRoot 返回本地 Mock 存储根目录;生产环境可通过环境变量映射到受控挂载目录。
|
||||
func uploadRoot() string {
|
||||
if directory := strings.TrimSpace(os.Getenv("HEQI_UPLOAD_DIR")); directory != "" {
|
||||
return directory
|
||||
}
|
||||
return filepath.Join("runtime", "uploads")
|
||||
}
|
||||
Reference in New Issue
Block a user