Files
platforms/backend/api/internal/logic/gas/user.go

429 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gas
import (
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/upload"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// gasUserListItem 在用户主档上附加当前气站服务关系中的配送点归属。
type gasUserListItem struct {
models.UserAccount
DeliveryBasicID uint64 `gorm:"column:delivery_basic_id" json:"delivery_basic_id"`
}
// gasUserDetail 标识用户是否仅因当前气站合同而可见,前端据此进入只读模式。
type gasUserDetail struct {
models.UserAccount
ContractReadonly bool `json:"contract_readonly"`
}
// scopedUserList 将用户列表与当前气站唯一有效服务关系绑定,避免返回跨站归属。
func scopedUserList(databaseService *gorm.DB, gasBasicID uint64) *gorm.DB {
return common.ActiveRecords(databaseService.Model(&models.UserAccount{})).
Select("user_account.*, user_service_relation.delivery_basic_id AS delivery_basic_id").
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?", common.StatusArchived).
Where("user_service_relation.gas_basic_id = ?", gasBasicID)
}
func ListUser(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
page, size := common.PageSize(ctx)
var list []gasUserListItem
var total int64
query := common.ApplyKeywordFilter(ctx,
scopedUserList(impl.DBService, station.ID),
&models.UserAccount{})
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
if err := query.Order("user_account.created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
respondList(ctx, list, total)
}
func GetUser(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
user, relation, err := findCurrentGasUser(impl.DBService, ctx.Param("identity"), station.ID)
contractReadonly := false
var contractDeliveryBasicID uint64
if err != nil {
contractUser, contractErr := findContractScopedUser(
impl.DBService, ctx.Param("identity"), station.ID, ctx.Query("contract_identity"),
)
err = contractErr
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user = contractUser.UserAccount
contractReadonly = true
contractDeliveryBasicID = contractUser.ContractDeliveryBasicID
}
if contractReadonly {
// 合同范围只返回用户主档,不泄露地址、钱包或其他服务关系上下文。
detail := gasUserDetail{UserAccount: user, ContractReadonly: true}
response, responseErr := common.PublicResourceResponse(gin.H{
"user": detail,
"relation": gin.H{"delivery_basic_id": contractDeliveryBasicID},
"addresses": []models.UserAddress{},
})
if responseErr != nil {
infra.Response.Error(ctx, responseErr)
return
}
infra.Response.Success(ctx, response)
return
}
var addresses []models.UserAddress
if err := common.ActiveRecords(impl.DBService).Where("user_account_id = ?", user.ID).Order("is_default desc, created_at desc").Find(&addresses).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
response, err := common.PublicResourceResponse(gin.H{"user": user, "relation": relation, "addresses": addresses})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, response)
}
// GetUserAvatar 返回与当前气站存在服务关系或合同引用关系的用户受保护头像。
func GetUserAvatar(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
user, _, err := findCurrentGasUser(impl.DBService, ctx.Param("identity"), station.ID)
if err != nil {
contractUser, contractErr := findContractScopedUser(impl.DBService, ctx.Param("identity"), station.ID, "")
err = contractErr
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user = contractUser.UserAccount
}
upload.ServeAvatar(ctx, user.Avatar)
}
type userRequest struct {
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name" binding:"required,max=64"`
Phone string `json:"phone" binding:"max=32"`
Avatar *string `json:"avatar" binding:"omitempty,max=512"`
RealName string `json:"real_name" binding:"max=64"`
DeliveryBasicIdentity string `json:"delivery_basic_identity" binding:"required"`
}
func CreateUser(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
var request userRequest
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" || !common.IsValidAccountPassword(request.Password) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
delivery, ok := requireDelivery(ctx, request.DeliveryBasicIdentity, station.ID)
if !ok {
return
}
hash, err := common.PasswordHash(request.Password)
if err != nil {
infra.Response.Error(ctx, err)
return
}
avatar := ""
if request.Avatar != nil {
avatar = *request.Avatar
}
user := models.UserAccount{
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
Name: request.Name, Phone: request.Phone, Avatar: avatar, RealName: request.RealName,
}
relation := models.UserServiceRelation{Entity: common.NewEntity(common.StatusEnable), GasBasicID: station.ID, DeliveryBasicID: delivery.ID}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
relation.UserAccountID = user.ID
return tx.Create(&relation).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, user)
}
func UpdateUser(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
user, relation, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
if !ok {
return
}
var request userRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
delivery, ok := requireDelivery(ctx, request.DeliveryBasicIdentity, station.ID)
if !ok {
return
}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
values := map[string]any{
"name": request.Name, "phone": request.Phone, "real_name": request.RealName,
}
// 未选择新头像时不提交 avatar避免编辑基础资料误清空现有头像。
if request.Avatar != nil {
values["avatar"] = *request.Avatar
}
if err := tx.Model(&user).Updates(values).Error; err != nil {
return err
}
return tx.Model(&relation).Update("delivery_basic_id", delivery.ID).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
func UpdateUserStatus(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
if _, _, ok := requireUser(ctx, ctx.Param("identity"), station.ID); !ok {
return
}
common.UpdateRecordStatus(ctx, &models.UserAccount{})
}
func ArchiveUser(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
user, relation, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
if !ok {
return
}
var blocking int64
if err := impl.DBService.Model(&models.GasorderBasic{}).
Where("user_account_id = ? AND order_status NOT IN ?", user.ID, []int{common.StatusCompleted, common.StatusCancelled}).
Count(&blocking).Error; err != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Model(&models.CsTicket{}).
Where("user_account_id = ? AND status <> ? AND ticket_status = ?", user.ID, common.StatusArchived, common.StatusOpen).
Count(&blocking).Error; err != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var wallet models.WalletBasic
err := impl.DBService.Where("owner_type = ? AND owner_id = ? AND status <> ?", "user", user.ID, common.StatusArchived).First(&wallet).Error
if err == nil && (wallet.Balance > 0 || wallet.WithdrawalBalance > 0) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err == nil {
if cashErr := impl.DBService.Model(&models.WalletApplyCash{}).
Where("wallet_basic_id = ? AND status <> ? AND apply_status = ?", wallet.ID, common.StatusArchived, common.StatusPending).
Count(&blocking).Error; cashErr != nil || blocking > 0 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
}
if err != nil && err != gorm.ErrRecordNotFound {
infra.Response.Error(ctx, err)
return
}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&relation).Update("status", common.StatusArchived).Error; err != nil {
return err
}
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ? AND status <> ?", user.ID, common.StatusArchived).
Update("status", common.StatusArchived).Error; err != nil {
return err
}
return tx.Model(&user).Update("status", common.StatusArchived).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"archived": true})
}
func ListUserAddress(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
page, size := common.PageSize(ctx)
var list []models.UserAddress
var total int64
query := common.ActiveRecords(impl.DBService.Model(&models.UserAddress{})).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_service_relation.gas_basic_id = ?", station.ID)
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
if err := query.Order("user_address.created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
respondList(ctx, list, total)
}
func GetUserAddress(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
var address models.UserAddress
query := common.ActiveRecords(impl.DBService.Model(&models.UserAddress{})).
Select("user_address.*").
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_address.identity = ? AND user_service_relation.gas_basic_id = ?", ctx.Param("identity"), station.ID)
respondScopedRecord(ctx, query, &address)
}
type addressRequest struct {
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
Address string `json:"address" binding:"required,max=255"`
Longitude string `json:"longitude" binding:"max=32"`
Latitude string `json:"latitude" binding:"max=32"`
IsDefault bool `json:"is_default"`
}
func CreateUserAddress(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user, _, ok := requireUser(ctx, request.UserAccountIdentity, station.ID)
if !ok {
return
}
address := models.UserAddress{
Entity: common.NewEntity(common.StatusEnable), UserAccountID: user.ID, Address: request.Address,
Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault,
}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", user.ID).Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Create(&address).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, address)
}
func UpdateUserAddress(ctx *gin.Context) {
station, ok := currentGas(ctx)
if !ok {
return
}
var existing models.UserAddress
if err := common.ActiveRecords(impl.DBService.Model(&models.UserAddress{})).
Select("user_address.*").
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_address.identity = ? AND user_service_relation.gas_basic_id = ?", ctx.Param("identity"), station.ID).
First(&existing).Error; err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
user, _, ok := requireUser(ctx, request.UserAccountIdentity, station.ID)
if !ok || user.ID != existing.UserAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ? AND id <> ?", user.ID, existing.ID).
Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Model(&existing).Updates(map[string]any{
"address": request.Address, "longitude": request.Longitude, "latitude": request.Latitude, "is_default": request.IsDefault,
}).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
func UpdateUserAddressStatus(ctx *gin.Context) {
if !requireScopedAddress(ctx) {
return
}
common.UpdateRecordStatus(ctx, &models.UserAddress{})
}
func ArchiveUserAddress(ctx *gin.Context) {
if !requireScopedAddress(ctx) {
return
}
common.ArchiveRecord(ctx, &models.UserAddress{})
}
func requireScopedAddress(ctx *gin.Context) bool {
station, ok := currentGas(ctx)
if !ok {
return false
}
var count int64
err := impl.DBService.Model(&models.UserAddress{}).
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_address.user_account_id AND user_service_relation.status <> ?",
common.StatusArchived).
Where("user_address.identity = ? AND user_service_relation.gas_basic_id = ?", ctx.Param("identity"), station.ID).
Count(&count).Error
if err != nil || count != 1 {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return false
}
return true
}