feat: implement gas and delivery admin systems
This commit is contained in:
138
backend/api/internal/logic/delivery/auth.go
Normal file
138
backend/api/internal/logic/delivery/auth.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/crypto/token"
|
||||
"git.apinb.com/bsm-sdk/core/env"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func Login(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
if err := impl.DBService.Where("username = ?", strings.TrimSpace(request.Username)).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
var point models.DeliveryBasic
|
||||
var station models.GasBasic
|
||||
if account.Status != common.StatusEnable || account.RoleCode != "admin" ||
|
||||
impl.DBService.First(&point, account.DeliveryBasicID).Error != nil || point.Status != common.StatusEnable ||
|
||||
impl.DBService.First(&station, point.GasBasicID).Error != nil || station.Status != common.StatusEnable ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.Password)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
accessToken, err := token.New(env.Runtime.JwtSecretKey).GenerateJwt(
|
||||
0, account.Identity, "delivery_admin", account.RoleCode, nil,
|
||||
map[string]string{"delivery_basic_identity": point.Identity, "gas_basic_identity": station.Identity, "username": account.Username, "display_name": account.DisplayName},
|
||||
)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"access_token": accessToken, "token_type": "JWT", "identity": account.Identity,
|
||||
"display_name": account.DisplayName, "role_code": account.RoleCode,
|
||||
"delivery_basic_identity": point.Identity, "gas_basic_identity": station.Identity,
|
||||
})
|
||||
}
|
||||
|
||||
func RequireDeliveryAdmin() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil || claims.Client != "delivery_admin" || claims.Role != "admin" || claims.Extend["delivery_basic_identity"] == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
err = impl.DBService.Model(&models.DeliveryAccount{}).
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_account.status = ? AND delivery_account.role_code = ? AND delivery_basic.identity = ? AND delivery_basic.status = ?",
|
||||
claims.Identity, common.StatusEnable, "admin", claims.Extend["delivery_basic_identity"], common.StatusEnable).
|
||||
Count(&count).Error
|
||||
if err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func CurrentDeliveryAccount(ctx *gin.Context) (models.DeliveryAccount, models.DeliveryBasic, models.GasBasic, bool) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return models.DeliveryAccount{}, models.DeliveryBasic{}, models.GasBasic{}, false
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
var point models.DeliveryBasic
|
||||
var station models.GasBasic
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil ||
|
||||
impl.DBService.First(&point, account.DeliveryBasicID).Error != nil ||
|
||||
impl.DBService.First(&station, point.GasBasicID).Error != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return account, point, station, false
|
||||
}
|
||||
return account, point, station, true
|
||||
}
|
||||
|
||||
func CurrentProfile(ctx *gin.Context) {
|
||||
account, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
menus := MenusForRole(account.RoleCode)
|
||||
codes := make([]string, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
codes = append(codes, menu.Identity)
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"identity": account.Identity, "username": account.Username, "display_name": account.DisplayName,
|
||||
"role_code": account.RoleCode, "delivery_basic_identity": point.Identity, "delivery_basic_name": point.Name,
|
||||
"gas_basic_identity": station.Identity, "menu_codes": codes,
|
||||
})
|
||||
}
|
||||
|
||||
func ChangePassword(ctx *gin.Context) {
|
||||
account, _, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.NewPassword) ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&account).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
89
backend/api/internal/logic/delivery/base.go
Normal file
89
backend/api/internal/logic/delivery/base.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func currentScope(ctx *gin.Context) (models.DeliveryBasic, models.GasBasic, bool) {
|
||||
_, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
return point, station, ok
|
||||
}
|
||||
|
||||
func ListMenu(ctx *gin.Context) {
|
||||
account, _, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
menus := MenusForRole(account.RoleCode)
|
||||
infra.Response.Success(ctx, gin.H{"total": len(menus), "list": menus})
|
||||
}
|
||||
|
||||
func InvitationQRCode(ctx *gin.Context) {
|
||||
_, point, station, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
registerURL, _ := url.Parse(config.Spec.Global.UserRegisterURL)
|
||||
query := registerURL.Query()
|
||||
query.Set("gas_identity", station.Identity)
|
||||
query.Set("delivery_identity", point.Identity)
|
||||
registerURL.RawQuery = query.Encode()
|
||||
infra.Response.Success(ctx, gin.H{"register_url": registerURL.String()})
|
||||
}
|
||||
|
||||
func ListProfile(ctx *gin.Context) {
|
||||
_, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondList(ctx, []models.DeliveryBasic{point}, 1)
|
||||
}
|
||||
|
||||
func respondList(ctx *gin.Context, list any, total int64) {
|
||||
response, err := common.PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
|
||||
}
|
||||
|
||||
func listScoped(ctx *gin.Context, model any, query *gorm.DB, order string) {
|
||||
page, size := common.PageSize(ctx)
|
||||
query = common.ApplyKeywordFilter(ctx, query, model)
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
list := reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem()))
|
||||
if err := query.Order(order).Offset((page - 1) * size).Limit(size).Find(list.Interface()).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondList(ctx, list.Elem().Interface(), total)
|
||||
}
|
||||
|
||||
func respondRecord(ctx *gin.Context, query *gorm.DB, model any) {
|
||||
if err := query.First(model).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(model)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
func db() *gorm.DB { return impl.DBService }
|
||||
55
backend/api/internal/logic/delivery/dashboard.go
Normal file
55
backend/api/internal/logic/delivery/dashboard.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Dashboard(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result := gin.H{}
|
||||
queries := []struct {
|
||||
key string
|
||||
model any
|
||||
where string
|
||||
args []any
|
||||
}{
|
||||
{"staff_count", &models.StaffAccount{}, "delivery_basic_id = ? AND role_code = ? AND status <> ?", []any{point.ID, "delivery", common.StatusArchived}},
|
||||
{"user_count", &models.UserServiceRelation{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
{"contract_count", &models.GasorderContract{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
{"order_count", &models.GasorderBasic{}, "delivery_basic_id = ? AND status <> ?", []any{point.ID, common.StatusArchived}},
|
||||
}
|
||||
for _, query := range queries {
|
||||
var count int64
|
||||
if err := db().Model(query.model).Where(query.where, query.args...).Count(&count).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
result[query.key] = count
|
||||
}
|
||||
infra.Response.Success(ctx, result)
|
||||
}
|
||||
|
||||
func DashboardReport(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
type row struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var rows []row
|
||||
if err := db().Model(&models.GasorderBasic{}).Select("order_status AS status, count(*) AS count").
|
||||
Where("delivery_basic_id = ? AND status <> ?", point.ID, common.StatusArchived).
|
||||
Group("order_status").Order("order_status").Scan(&rows).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"orders_by_status": rows})
|
||||
}
|
||||
210
backend/api/internal/logic/delivery/finance.go
Normal file
210
backend/api/internal/logic/delivery/finance.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func currentWallet(ctx *gin.Context, point models.DeliveryBasic) (models.WalletBasic, bool) {
|
||||
var wallet models.WalletBasic
|
||||
if err := common.ActiveRecords(db()).Where("owner_type = ? AND owner_id = ? AND owner_identity = ?",
|
||||
"delivery", point.ID, point.Identity).First(&wallet).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return wallet, false
|
||||
}
|
||||
return wallet, true
|
||||
}
|
||||
|
||||
func ListWallet(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.WalletBasic{}, common.ActiveRecords(db().Model(&models.WalletBasic{})).
|
||||
Where("owner_type = ? AND owner_id = ?", "delivery", point.ID), "wallet_basic.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetWallet(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where(
|
||||
"identity = ? AND owner_type = ? AND owner_id = ?", ctx.Param("identity"), "delivery", point.ID), &wallet)
|
||||
}
|
||||
|
||||
func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(model)).
|
||||
Joins("JOIN wallet_basic ON wallet_basic.id = "+table+".wallet_basic_id").
|
||||
Where("wallet_basic.owner_type = ? AND wallet_basic.owner_id = ?", "delivery", point.ID)
|
||||
listScoped(ctx, model, query, table+".created_at desc")
|
||||
}
|
||||
|
||||
func ListBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListPayment(ctx *gin.Context) { listWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func ListRecord(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func ListRefund(ctx *gin.Context) { listWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func ListApplyCash(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
}
|
||||
|
||||
func getWalletChild(ctx *gin.Context, model any, table string) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(model)).
|
||||
Joins("JOIN wallet_basic ON wallet_basic.id = "+table+".wallet_basic_id").
|
||||
Where(table+".identity = ? AND wallet_basic.owner_type = ? AND wallet_basic.owner_id = ?",
|
||||
ctx.Param("identity"), "delivery", point.ID)
|
||||
respondRecord(ctx, query, model)
|
||||
}
|
||||
|
||||
func GetBank(ctx *gin.Context) { getWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func GetPayment(ctx *gin.Context) { getWalletChild(ctx, &models.WalletPayment{}, "wallet_payment") }
|
||||
func GetRecord(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRecord{}, "wallet_record") }
|
||||
func GetRefund(ctx *gin.Context) { getWalletChild(ctx, &models.WalletRefund{}, "wallet_refund") }
|
||||
func GetApplyCash(ctx *gin.Context) {
|
||||
getWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
}
|
||||
|
||||
func Recharge(ctx *gin.Context) {
|
||||
account, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required,max=1000"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Amount <= 0 ||
|
||||
request.Amount > config.Spec.Global.ManualRechargeMaxAmount ||
|
||||
strings.TrimSpace(request.RequestNo) == "" || strings.TrimSpace(request.Reason) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var record models.WalletRecord
|
||||
err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("request_no = ?", request.RequestNo).First(&record).Error; err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("owner_type = ? AND owner_id = ? AND owner_identity = ?", "delivery", point.ID, point.Identity).
|
||||
First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.Model(&wallet).Where("status = ? AND balance <= ?", common.StatusEnable, math.MaxInt64-request.Amount).
|
||||
Update("balance", gorm.Expr("balance + ?", request.Amount))
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
return errors.New("wallet disabled or balance overflow")
|
||||
}
|
||||
if err := tx.First(&wallet, wallet.ID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
record = models.WalletRecord{
|
||||
Entity: common.NewEntity(common.StatusEnable), WalletBasicID: wallet.ID,
|
||||
RecordNo: models.NewIdentity(), RequestNo: request.RequestNo, Direction: "income",
|
||||
TradeType: "delivery_admin_recharge", Amount: request.Amount,
|
||||
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: request.RequestNo, PayChannel: "delivery_admin",
|
||||
OperatorIdentity: account.Identity, OperatorName: account.DisplayName,
|
||||
Ymd: int32(now.Year()*10000 + int(now.Month())*100 + now.Day()),
|
||||
Ym: int32(now.Year()*100 + int(now.Month())),
|
||||
Remark: strings.TrimSpace(request.Reason + " " + request.Remark),
|
||||
}
|
||||
return tx.Create(&record).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, record)
|
||||
}
|
||||
|
||||
func CreateApplyCash(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wallet, ok := currentWallet(ctx, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
WalletBankIdentity string `json:"wallet_bank_identity"`
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,max=32"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Amount <= 0 || strings.TrimSpace(request.RequestNo) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var pending int64
|
||||
if err := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
|
||||
wallet.ID, common.StatusPending, common.StatusArchived).Select("COALESCE(SUM(amount), 0)").Scan(&pending).Error; err != nil ||
|
||||
request.Amount > wallet.WithdrawalBalance-pending {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND wallet_basic_id = ?",
|
||||
request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
apply := models.WalletApplyCash{
|
||||
Entity: common.NewEntity(common.StatusEnable), ApplyStatus: common.StatusPending,
|
||||
WalletBasicID: wallet.ID, WalletBankID: bankID, CashNo: models.NewIdentity(), RequestNo: request.RequestNo,
|
||||
Amount: request.Amount, Channel: request.Channel, Remark: request.Remark,
|
||||
}
|
||||
if err := db().Create(&apply).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, apply)
|
||||
}
|
||||
|
||||
func ListSettlement(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.FinSettlement{}, common.ActiveRecords(db().Model(&models.FinSettlement{})).
|
||||
Where("subject_type = ? AND subject_id = ?", "delivery", point.ID), "fin_settlement.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetSettlement(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var settlement models.FinSettlement
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where(
|
||||
"identity = ? AND subject_type = ? AND subject_id = ?", ctx.Param("identity"), "delivery", point.ID), &settlement)
|
||||
}
|
||||
50
backend/api/internal/logic/delivery/menu.go
Normal file
50
backend/api/internal/logic/delivery/menu.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package delivery
|
||||
|
||||
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
|
||||
type Menu struct {
|
||||
Identity string `json:"identity"`
|
||||
ParentIdentity string `json:"parent_identity,omitempty"`
|
||||
GroupCode string `json:"group_code"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Path string `json:"path"`
|
||||
SortNo int `json:"sort_no"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
var adminMenus = []Menu{
|
||||
{Identity: "dashboard", GroupCode: "dashboard", Name: "数据概述", Icon: "icon-dashboard", Path: "/dashboard", SortNo: 10, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_overview", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "运营概览", Path: "/dashboard/overview", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "订单报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "profile", GroupCode: "profile", Name: "配送点资料", Icon: "icon-storage", Path: "/profile", SortNo: 20, Status: common.StatusEnable},
|
||||
{Identity: "delivery_profile", ParentIdentity: "profile", GroupCode: "profile", Name: "本点资料", Path: "/profile/basic", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff", GroupCode: "staff", Name: "配送人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 30, Status: common.StatusEnable},
|
||||
{Identity: "staff_add", ParentIdentity: "staff", GroupCode: "staff", Name: "新增配送人员", Path: "/staff/add", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff_delivery", ParentIdentity: "staff", GroupCode: "staff", Name: "配送人员列表", Path: "/staff/delivery", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "user", GroupCode: "user", Name: "用户管理", Icon: "icon-user", Path: "/user", SortNo: 40, Status: common.StatusEnable},
|
||||
{Identity: "user_account", ParentIdentity: "user", GroupCode: "user", Name: "用户账户", Path: "/user/accounts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "contract", GroupCode: "contract", Name: "合同管理", Icon: "icon-file", Path: "/contract", SortNo: 50, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_contract", ParentIdentity: "contract", GroupCode: "contract", Name: "配送合同", Path: "/contract/contracts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder", GroupCode: "gasorder", Name: "配送订单", Icon: "icon-list", Path: "/gasorder", SortNo: 60, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_create", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "创建订单", Path: "/gasorder/create", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_basic", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "订单列表", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 70, Status: common.StatusEnable},
|
||||
{Identity: "wallet_basic", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包", Path: "/finance/wallet", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "wallet_bank", ParentIdentity: "finance", GroupCode: "finance", Name: "银行卡", Path: "/finance/banks", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "wallet_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "wallet_record", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包流水", Path: "/finance/records", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "wallet_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "wallet_recharge", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包充值", Path: "/finance/recharge", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现申请", Path: "/finance/withdrawals", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "结算结果", Path: "/finance/settlements", SortNo: 8, Status: common.StatusEnable},
|
||||
{Identity: "invitation", GroupCode: "invitation", Name: "邀请注册", Icon: "icon-qrcode", Path: "/invitation", SortNo: 80, Status: common.StatusEnable},
|
||||
{Identity: "invitation_qrcode", ParentIdentity: "invitation", GroupCode: "invitation", Name: "邀请二维码", Path: "/invitation/qrcode", SortNo: 1, Status: common.StatusEnable},
|
||||
}
|
||||
|
||||
func MenusForRole(roleCode string) []Menu {
|
||||
if roleCode != "admin" {
|
||||
return nil
|
||||
}
|
||||
return append([]Menu(nil), adminMenus...)
|
||||
}
|
||||
23
backend/api/internal/logic/delivery/menu_test.go
Normal file
23
backend/api/internal/logic/delivery/menu_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package delivery
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMenusOnlyAllowAdmin(t *testing.T) {
|
||||
if len(MenusForRole("admin")) == 0 {
|
||||
t.Fatal("admin must receive delivery menus")
|
||||
}
|
||||
for _, role := range []string{"root", "dispatcher", "warehouse", "quality", "delivery", ""} {
|
||||
if len(MenusForRole(role)) != 0 {
|
||||
t.Fatalf("role %q must not receive delivery admin menus", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenusExcludeInventoryAndEcommerce(t *testing.T) {
|
||||
for _, menu := range MenusForRole("admin") {
|
||||
switch menu.GroupCode {
|
||||
case "inventory", "warehouse", "ec":
|
||||
t.Fatalf("forbidden menu group %q", menu.GroupCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
406
backend/api/internal/logic/delivery/order.go
Normal file
406
backend/api/internal/logic/delivery/order.go
Normal file
@@ -0,0 +1,406 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
platformgasorder "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gasorder"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func rewriteJSON(ctx *gin.Context, mutate func(map[string]any) bool) bool {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return false
|
||||
}
|
||||
var values map[string]any
|
||||
if json.Unmarshal(body, &values) != nil || !mutate(values) {
|
||||
if !ctx.IsAborted() {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
return false
|
||||
}
|
||||
body, _ = json.Marshal(values)
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return true
|
||||
}
|
||||
|
||||
func scopedContract(ctx *gin.Context, identity string, pointID uint64) (models.GasorderContract, bool) {
|
||||
var contract models.GasorderContract
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND delivery_basic_id = ?", identity, pointID).First(&contract).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return contract, false
|
||||
}
|
||||
return contract, true
|
||||
}
|
||||
|
||||
func ListContract(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.GasorderContract{}, common.ActiveRecords(db().Model(&models.GasorderContract{})).
|
||||
Where("gas_basic_id = ? AND delivery_basic_id = ?", point.GasBasicID, point.ID), "gasorder_contract.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetContract(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
|
||||
platformgasorder.GetGasorderContract(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CreateContract(ctx *gin.Context) {
|
||||
point, station, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
userIdentity, _ := values["user_account_identity"].(string)
|
||||
if _, _, valid := scopedUser(ctx, userIdentity, point.ID); !valid {
|
||||
return false
|
||||
}
|
||||
values["gas_basic_identity"] = station.Identity
|
||||
values["delivery_basic_identity"] = point.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.CreateGasorderContract(ctx)
|
||||
}
|
||||
|
||||
func withContract(ctx *gin.Context, handler gin.HandlerFunc) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); valid {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateContract(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, valid := scopedContract(ctx, ctx.Param("identity"), point.ID); !valid {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
values["delivery_basic_identity"] = point.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.UpdateGasorderContract(ctx)
|
||||
}
|
||||
func ActivateContract(ctx *gin.Context) { withContract(ctx, platformgasorder.ActivateGasorderContract) }
|
||||
func RenewContract(ctx *gin.Context) { withContract(ctx, platformgasorder.RenewGasorderContract) }
|
||||
func TerminateContract(ctx *gin.Context) {
|
||||
withContract(ctx, platformgasorder.TerminateGasorderContract)
|
||||
}
|
||||
|
||||
func ListContractProduct(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.GasorderContractProduct{})).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract.delivery_basic_id = ?", point.ID)
|
||||
listScoped(ctx, &models.GasorderContractProduct{}, query, "gasorder_contract_product.created_at desc")
|
||||
}
|
||||
|
||||
func GetContractProduct(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var item models.GasorderContractProduct
|
||||
query := common.ActiveRecords(db().Model(&models.GasorderContractProduct{})).
|
||||
Select("gasorder_contract_product.*").
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract_product.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
|
||||
respondRecord(ctx, query, &item)
|
||||
}
|
||||
|
||||
func BindContractProduct(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
identity, _ := values["gasorder_contract_identity"].(string)
|
||||
_, valid := scopedContract(ctx, identity, point.ID)
|
||||
return valid
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.BindGasorderContractProduct(ctx)
|
||||
}
|
||||
|
||||
func UnbindContractProduct(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
err := db().Model(&models.GasorderContractProduct{}).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract_product.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID).
|
||||
Count(&count).Error
|
||||
if err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
platformgasorder.UnbindGasorderContractProduct(ctx)
|
||||
}
|
||||
|
||||
func ListContractRevision(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.GasorderContractRevision{})).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_revision.gasorder_contract_id").
|
||||
Where("gasorder_contract.delivery_basic_id = ?", point.ID)
|
||||
listScoped(ctx, &models.GasorderContractRevision{}, query, "gasorder_contract_revision.created_at desc")
|
||||
}
|
||||
|
||||
func GetContractRevision(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var item models.GasorderContractRevision
|
||||
query := common.ActiveRecords(db().Model(&models.GasorderContractRevision{})).
|
||||
Select("gasorder_contract_revision.*").
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_revision.gasorder_contract_id").
|
||||
Where("gasorder_contract_revision.identity = ? AND gasorder_contract.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
|
||||
respondRecord(ctx, query, &item)
|
||||
}
|
||||
|
||||
func ListProductCandidate(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.ProductInfo{})).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).Where("user_service_relation.delivery_basic_id = ?", point.ID)
|
||||
listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc")
|
||||
}
|
||||
|
||||
func GetProductCandidate(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var item models.ProductInfo
|
||||
query := common.ActiveRecords(db().Model(&models.ProductInfo{})).
|
||||
Select("product_info.*").
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("product_info.identity = ? AND user_service_relation.delivery_basic_id = ?", ctx.Param("identity"), point.ID)
|
||||
respondRecord(ctx, query, &item)
|
||||
}
|
||||
|
||||
func scopedOrder(ctx *gin.Context, identity string, pointID uint64) (models.GasorderBasic, bool) {
|
||||
var order models.GasorderBasic
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND delivery_basic_id = ?", identity, pointID).First(&order).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return order, false
|
||||
}
|
||||
return order, true
|
||||
}
|
||||
|
||||
func ListOrder(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.GasorderBasic{}, common.ActiveRecords(db().Model(&models.GasorderBasic{})).
|
||||
Where("delivery_basic_id = ?", point.ID), "gasorder_basic.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetOrder(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); valid {
|
||||
platformgasorder.GetGasorderBasic(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CreateOrder(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
identity, _ := values["gasorder_contract_identity"].(string)
|
||||
if _, valid := scopedContract(ctx, identity, point.ID); !valid {
|
||||
return false
|
||||
}
|
||||
values["creator_type"] = "delivery"
|
||||
values["creator_identity"] = point.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.CreateGasorderBasic(ctx)
|
||||
}
|
||||
|
||||
func AssignOrder(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); !valid {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
staffIdentity, _ := values["staff_account_identity"].(string)
|
||||
staff, valid := scopedStaff(ctx, staffIdentity, point)
|
||||
if !valid || staff.WorkStatus != "on_duty" || staff.Status != common.StatusEnable {
|
||||
return false
|
||||
}
|
||||
var credentialCount int64
|
||||
err := common.ActiveRecords(db().Model(&models.StaffCredential{})).
|
||||
Where("staff_account_id = ? AND (expired_at IS NULL OR expired_at > ?)", staff.ID, time.Now()).
|
||||
Count(&credentialCount).Error
|
||||
if err != nil || credentialCount == 0 {
|
||||
return false
|
||||
}
|
||||
values["delivery_basic_identity"] = point.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.AssignGasorderBasic(ctx)
|
||||
}
|
||||
|
||||
func withOrder(ctx *gin.Context, handler gin.HandlerFunc) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
if _, valid := scopedOrder(ctx, ctx.Param("identity"), point.ID); valid {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func OrderException(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderException) }
|
||||
func OrderRecover(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderRecover) }
|
||||
func OrderCancel(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderCancel) }
|
||||
|
||||
func ReclaimOrder(ctx *gin.Context) {
|
||||
account, point, _, ok := CurrentDeliveryAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Reason string `json:"reason" binding:"required,max=1000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
err := db().Transaction(func(tx *gorm.DB) error {
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("identity = ? AND delivery_basic_id = ?", ctx.Param("identity"), point.ID).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OrderStatus != common.StatusAssigned {
|
||||
return errors.New("only assigned order can be reclaimed")
|
||||
}
|
||||
if err := tx.Model(&order).Updates(map[string]any{
|
||||
"staff_account_id": 0, "order_status": common.StatusCreated,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&models.GasorderStatus{
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
||||
FromStatus: common.StatusAssigned, ToStatus: common.StatusCreated,
|
||||
OperatorIdentity: account.Identity, OperatorName: account.DisplayName,
|
||||
OccurredAt: time.Now(), Reason: request.Reason,
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusCreated})
|
||||
}
|
||||
|
||||
func AdjustOrderAmount(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeliveryFee int64 `json:"delivery_fee"`
|
||||
DiscountAmount int64 `json:"discount_amount"`
|
||||
Reason string `json:"reason" binding:"required,max=1000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.DeliveryFee < 0 || request.DiscountAmount < 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
account, _, _, _ := CurrentDeliveryAccount(ctx)
|
||||
err := db().Transaction(func(tx *gorm.DB) error {
|
||||
var order models.GasorderBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("identity = ? AND delivery_basic_id = ?", ctx.Param("identity"), point.ID).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OrderStatus != common.StatusCreated && order.OrderStatus != common.StatusAssigned {
|
||||
return errors.New("order amount cannot be adjusted")
|
||||
}
|
||||
var paymentCount int64
|
||||
if err := tx.Model(&models.GasorderPayment{}).
|
||||
Joins("JOIN wallet_payment ON wallet_payment.id = gasorder_payment.wallet_payment_id").
|
||||
Where("gasorder_payment.gasorder_basic_id = ? AND wallet_payment.payment_status = ?", order.ID, common.StatusPaid).
|
||||
Count(&paymentCount).Error; err != nil || paymentCount > 0 {
|
||||
return errors.New("paid order cannot be adjusted")
|
||||
}
|
||||
if order.ProductAmount > math.MaxInt64-request.DeliveryFee {
|
||||
return errors.New("payable amount overflow")
|
||||
}
|
||||
payable := order.ProductAmount + request.DeliveryFee - request.DiscountAmount
|
||||
if payable <= 0 {
|
||||
return errors.New("payable amount must be positive")
|
||||
}
|
||||
if err := tx.Model(&order).Updates(map[string]any{
|
||||
"delivery_fee": request.DeliveryFee, "discount_amount": request.DiscountAmount, "payable_amount": payable,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
record := models.GasorderStatus{
|
||||
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
||||
FromStatus: order.OrderStatus, ToStatus: order.OrderStatus,
|
||||
OperatorIdentity: account.Identity, OperatorName: account.DisplayName, OccurredAt: time.Now(),
|
||||
Reason: fmt.Sprintf("%s;配送费 %d→%d,优惠金额 %d→%d,应付金额 %d→%d",
|
||||
request.Reason, order.DeliveryFee, request.DeliveryFee, order.DiscountAmount, request.DiscountAmount,
|
||||
order.PayableAmount, payable),
|
||||
}
|
||||
return tx.Create(&record).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
33
backend/api/internal/logic/delivery/resource_contract.go
Normal file
33
backend/api/internal/logic/delivery/resource_contract.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package delivery
|
||||
|
||||
type ResourceContract struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
PageKind string `json:"pageKind"`
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
|
||||
func ExpectedResources() []ResourceContract {
|
||||
items := []ResourceContract{
|
||||
{"profile", "delivery_profile", "/delivery_profile", "list", "readonly"},
|
||||
{"staff", "staff_account", "/staff_account", "list", "writable"},
|
||||
{"staff", "staff_credential", "/staff_credential", "list", "writable"},
|
||||
{"user", "user_account", "/user_account", "list", "writable"},
|
||||
{"user", "user_address", "/user_address", "list", "writable"},
|
||||
{"contract", "gasorder_contract", "/gasorder_contract", "list", "managed"},
|
||||
{"contract", "gasorder_contract_product", "/gasorder_contract_product", "list", "append_only"},
|
||||
{"contract", "gasorder_contract_revision", "/gasorder_contract_revision", "list", "readonly"},
|
||||
{"contract", "product_info", "/product_info", "list", "readonly"},
|
||||
{"gasorder", "gasorder_basic", "/gasorder_basic", "list", "append_only"},
|
||||
{"finance", "wallet_basic", "/wallet_basic", "list", "readonly"},
|
||||
{"finance", "wallet_bank", "/wallet_bank", "list", "readonly"},
|
||||
{"finance", "wallet_payment", "/wallet_payment", "list", "readonly"},
|
||||
{"finance", "wallet_record", "/wallet_record", "list", "readonly"},
|
||||
{"finance", "wallet_refund", "/wallet_refund", "list", "readonly"},
|
||||
{"finance", "wallet_recharge", "/wallet_recharge", "list", "append_only"},
|
||||
{"finance", "wallet_apply_cash", "/wallet_apply_cash", "list", "append_only"},
|
||||
{"finance", "fin_settlement", "/fin_settlement", "list", "readonly"},
|
||||
}
|
||||
return items
|
||||
}
|
||||
271
backend/api/internal/logic/delivery/staff.go
Normal file
271
backend/api/internal/logic/delivery/staff.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func scopedStaff(ctx *gin.Context, identity string, point models.DeliveryBasic) (models.StaffAccount, bool) {
|
||||
var staff models.StaffAccount
|
||||
if err := common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ?",
|
||||
identity, point.GasBasicID, point.ID).First(&staff).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return staff, false
|
||||
}
|
||||
return staff, true
|
||||
}
|
||||
|
||||
func ListStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(db().Model(&models.StaffAccount{})).
|
||||
Where("gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?", point.GasBasicID, point.ID, "delivery")
|
||||
listScoped(ctx, &models.StaffAccount{}, query, "staff_account.created_at desc")
|
||||
}
|
||||
|
||||
func GetStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var staff models.StaffAccount
|
||||
respondRecord(ctx, common.ActiveRecords(db()).Where("identity = ? AND gas_basic_id = ? AND delivery_basic_id = ? AND role_code = ?",
|
||||
ctx.Param("identity"), point.GasBasicID, point.ID, "delivery"), &staff)
|
||||
}
|
||||
|
||||
type staffRequest 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:"max=512"`
|
||||
WorkStatus string `json:"work_status" binding:"required"`
|
||||
}
|
||||
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" ||
|
||||
!common.IsValidAccountPassword(request.Password) || (request.WorkStatus != "on_duty" && request.WorkStatus != "off_duty") {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
staff := models.StaffAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: "delivery",
|
||||
GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID, WorkStatus: request.WorkStatus,
|
||||
}
|
||||
if err := db().Create(&staff).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, staff)
|
||||
}
|
||||
|
||||
func UpdateStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedStaff(ctx, ctx.Param("identity"), point); !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil ||
|
||||
(request.WorkStatus != "on_duty" && request.WorkStatus != "off_duty") {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "work_status": request.WorkStatus,
|
||||
}, []string{"name", "phone", "avatar", "work_status"})
|
||||
}
|
||||
|
||||
func ResetStaffPassword(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, ctx.Param("identity"), point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, _ := common.PasswordHash(request.Password)
|
||||
if err := db().Model(&staff).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
func UpdateStaffStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedStaff(ctx, ctx.Param("identity"), point); ok {
|
||||
common.UpdateRecordStatus(ctx, &models.StaffAccount{})
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveStaff(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, ctx.Param("identity"), point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := db().Model(&models.GasorderBasic{}).
|
||||
Where("staff_account_id = ? AND order_status NOT IN ?", staff.ID, []int{common.StatusCompleted, common.StatusCancelled}).
|
||||
Count(&count).Error; err != nil || count > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.StaffCredential{}).Where("staff_account_id = ?", staff.ID).
|
||||
Update("status", common.StatusArchived).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&staff).Update("status", common.StatusArchived).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
}
|
||||
|
||||
func credentialQuery(point models.DeliveryBasic) *gorm.DB {
|
||||
return common.ActiveRecords(db().Model(&models.StaffCredential{})).
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_account.delivery_basic_id = ? AND staff_account.gas_basic_id = ? AND staff_account.role_code = ?",
|
||||
point.ID, point.GasBasicID, "delivery")
|
||||
}
|
||||
|
||||
func ListCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.StaffCredential{}, credentialQuery(point), "staff_credential.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var credential models.StaffCredential
|
||||
respondRecord(ctx, credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")), &credential)
|
||||
}
|
||||
|
||||
type credentialRequest struct {
|
||||
StaffIdentity string `json:"staff_account_identity" binding:"required"`
|
||||
CredentialType string `json:"credential_type" binding:"required,max=64"`
|
||||
CredentialNo string `json:"credential_no" binding:"max=128"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func CreateCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, request.StaffIdentity, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
item := models.StaffCredential{
|
||||
Entity: common.NewEntity(common.StatusEnable), StaffAccountID: staff.ID,
|
||||
CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt,
|
||||
}
|
||||
if err := db().Create(&item).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, item)
|
||||
}
|
||||
|
||||
func UpdateCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var existing models.StaffCredential
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).First(&existing).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := scopedStaff(ctx, request.StaffIdentity, point)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := db().Model(&existing).Updates(map[string]any{
|
||||
"staff_account_id": staff.ID, "credential_type": request.CredentialType,
|
||||
"credential_no": request.CredentialNo, "expired_at": request.ExpiredAt,
|
||||
}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func UpdateCredentialStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.StaffCredential{})
|
||||
}
|
||||
|
||||
func ArchiveCredential(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := credentialQuery(point).Where("staff_credential.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.ArchiveRecord(ctx, &models.StaffCredential{})
|
||||
}
|
||||
328
backend/api/internal/logic/delivery/user.go
Normal file
328
backend/api/internal/logic/delivery/user.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func userQuery(pointID uint64) *gorm.DB {
|
||||
return common.ActiveRecords(db().Model(&models.UserAccount{})).
|
||||
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.delivery_basic_id = ?", pointID)
|
||||
}
|
||||
|
||||
func scopedUser(ctx *gin.Context, identity string, pointID uint64) (models.UserAccount, models.UserServiceRelation, bool) {
|
||||
var user models.UserAccount
|
||||
if err := userQuery(pointID).Where("user_account.identity = ?", identity).First(&user).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return user, models.UserServiceRelation{}, false
|
||||
}
|
||||
var relation models.UserServiceRelation
|
||||
if err := common.ActiveRecords(db()).Where("user_account_id = ? AND delivery_basic_id = ?", user.ID, pointID).First(&relation).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return user, relation, false
|
||||
}
|
||||
return user, relation, true
|
||||
}
|
||||
|
||||
func ListUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.UserAccount{}, userQuery(point.ID), "user_account.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var addresses []models.UserAddress
|
||||
if err := common.ActiveRecords(db()).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, _ := common.PublicResourceResponse(gin.H{"user": user, "relation": relation, "addresses": addresses})
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
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:"max=512"`
|
||||
RealName string `json:"real_name" binding:"max=64"`
|
||||
}
|
||||
|
||||
func CreateUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(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
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
user := models.UserAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RealName: request.RealName,
|
||||
}
|
||||
relation := models.UserServiceRelation{
|
||||
Entity: common.NewEntity(common.StatusEnable), GasBasicID: point.GasBasicID, DeliveryBasicID: point.ID,
|
||||
}
|
||||
if err := db().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) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request userRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().Model(&user).Updates(map[string]any{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName,
|
||||
}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func ResetUserPassword(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, _ := common.PasswordHash(request.Password)
|
||||
if err := db().Model(&user).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
func UpdateUserStatus(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, _, ok := scopedUser(ctx, ctx.Param("identity"), point.ID); ok {
|
||||
common.UpdateRecordStatus(ctx, &models.UserAccount{})
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveUser(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
user, relation, ok := scopedUser(ctx, ctx.Param("identity"), point.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var blocking int64
|
||||
if err := db().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 := db().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 := db().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 := db().Model(&models.WalletApplyCash{}).Where("wallet_basic_id = ? AND apply_status = ? AND status <> ?",
|
||||
wallet.ID, common.StatusPending, common.StatusArchived).Count(&blocking).Error; cashErr != nil || blocking > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := db().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 = ?", user.ID).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 addressQuery(pointID uint64) *gorm.DB {
|
||||
return common.ActiveRecords(db().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.delivery_basic_id = ?", pointID)
|
||||
}
|
||||
|
||||
func ListAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if ok {
|
||||
listScoped(ctx, &models.UserAddress{}, addressQuery(point.ID), "user_address.created_at desc")
|
||||
}
|
||||
}
|
||||
|
||||
func GetAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var address models.UserAddress
|
||||
respondRecord(ctx, addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")), &address)
|
||||
}
|
||||
|
||||
type addressRequest struct {
|
||||
UserIdentity 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 CreateAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request addressRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, request.UserIdentity, point.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 := db().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 UpdateAddress(ctx *gin.Context) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var existing models.UserAddress
|
||||
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).First(&existing).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var request addressRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
user, _, ok := scopedUser(ctx, request.UserIdentity, point.ID)
|
||||
if !ok || user.ID != existing.UserAccountID {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := db().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 UpdateAddressStatus(ctx *gin.Context) { mutateAddress(ctx, false) }
|
||||
func ArchiveAddress(ctx *gin.Context) { mutateAddress(ctx, true) }
|
||||
func mutateAddress(ctx *gin.Context, archive bool) {
|
||||
point, _, ok := currentScope(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := addressQuery(point.ID).Where("user_address.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if archive {
|
||||
common.ArchiveRecord(ctx, &models.UserAddress{})
|
||||
} else {
|
||||
common.UpdateRecordStatus(ctx, &models.UserAddress{})
|
||||
}
|
||||
}
|
||||
71
backend/api/internal/logic/gas/account_password.go
Normal file
71
backend/api/internal/logic/gas/account_password.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func resetScopedPassword(ctx *gin.Context, model any, scopeQuery func(models.GasBasic) bool) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !scopeQuery(station) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.Password) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(model).Where("identity = ?", ctx.Param("identity")).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
func ResetDeliveryAccountPassword(ctx *gin.Context) {
|
||||
resetScopedPassword(ctx, &models.DeliveryAccount{}, func(station models.GasBasic) bool {
|
||||
var count int64
|
||||
err := impl.DBService.Model(&models.DeliveryAccount{}).
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_account.status <> ? AND delivery_basic.gas_basic_id = ?",
|
||||
ctx.Param("identity"), common.StatusArchived, station.ID).Count(&count).Error
|
||||
return err == nil && count == 1
|
||||
})
|
||||
}
|
||||
|
||||
func ResetStaffPassword(ctx *gin.Context) {
|
||||
resetScopedPassword(ctx, &models.StaffAccount{}, func(station models.GasBasic) bool {
|
||||
var count int64
|
||||
err := impl.DBService.Model(&models.StaffAccount{}).
|
||||
Where("identity = ? AND status <> ? AND gas_basic_id = ?", ctx.Param("identity"), common.StatusArchived, station.ID).
|
||||
Count(&count).Error
|
||||
return err == nil && count == 1
|
||||
})
|
||||
}
|
||||
|
||||
func ResetUserPassword(ctx *gin.Context) {
|
||||
resetScopedPassword(ctx, &models.UserAccount{}, func(station models.GasBasic) bool {
|
||||
var count int64
|
||||
err := impl.DBService.Model(&models.UserAccount{}).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("user_account.identity = ? AND user_account.status <> ? AND user_service_relation.gas_basic_id = ?",
|
||||
ctx.Param("identity"), common.StatusArchived, station.ID).Count(&count).Error
|
||||
return err == nil && count == 1
|
||||
})
|
||||
}
|
||||
154
backend/api/internal/logic/gas/auth.go
Normal file
154
backend/api/internal/logic/gas/auth.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/crypto/token"
|
||||
"git.apinb.com/bsm-sdk/core/env"
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// Login 校验气站账号并签发独立 gas_admin JWT。
|
||||
func Login(ctx *gin.Context) {
|
||||
var request loginRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var account models.GasAccount
|
||||
err := impl.DBService.Where("username = ?", strings.TrimSpace(request.Username)).First(&account).Error
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
var station models.GasBasic
|
||||
if account.Status != common.StatusEnable || account.RoleCode != "admin" ||
|
||||
impl.DBService.First(&station, account.GasBasicID).Error != nil || station.Status != common.StatusEnable ||
|
||||
bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.Password)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
accessToken, err := token.New(env.Runtime.JwtSecretKey).GenerateJwt(
|
||||
0, account.Identity, "gas_admin", account.RoleCode, nil,
|
||||
map[string]string{"gas_basic_identity": station.Identity, "username": account.Username, "display_name": account.DisplayName},
|
||||
)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"access_token": accessToken, "token_type": "JWT", "identity": account.Identity,
|
||||
"display_name": account.DisplayName, "role_code": account.RoleCode, "gas_basic_identity": station.Identity,
|
||||
})
|
||||
}
|
||||
|
||||
// RequireGasAdmin 拒绝平台令牌和非本站管理员令牌。
|
||||
func RequireGasAdmin() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil || claims.Client != "gas_admin" || claims.Role != "admin" || claims.Extend["gas_basic_identity"] == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
err = impl.DBService.Model(&models.GasAccount{}).
|
||||
Joins("JOIN gas_basic ON gas_basic.id = gas_account.gas_basic_id").
|
||||
Where("gas_account.identity = ? AND gas_account.status = ? AND gas_account.role_code = ? AND gas_basic.identity = ? AND gas_basic.status = ?",
|
||||
claims.Identity, common.StatusEnable, "admin", claims.Extend["gas_basic_identity"], common.StatusEnable).
|
||||
Count(&count).Error
|
||||
if err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentProfile 返回气站账号、所属气站和菜单。
|
||||
func CurrentProfile(ctx *gin.Context) {
|
||||
account, station, ok := CurrentGasAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
menus := MenusForRole(account.RoleCode)
|
||||
menuCodes := make([]string, 0, len(menus))
|
||||
for _, menu := range menus {
|
||||
menuCodes = append(menuCodes, menu.Identity)
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"identity": account.Identity, "username": account.Username, "display_name": account.DisplayName,
|
||||
"role_code": account.RoleCode, "gas_basic_identity": station.Identity, "gas_basic_name": station.Name,
|
||||
"menu_codes": menuCodes,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword 修改当前气站账号密码。
|
||||
func ChangePassword(ctx *gin.Context) {
|
||||
var request struct {
|
||||
CurrentPassword string `json:"current_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsValidAccountPassword(request.NewPassword) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
claims, _ := middleware.ParseAuth(ctx)
|
||||
var account models.GasAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(request.CurrentPassword)) != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrPassword)
|
||||
return
|
||||
}
|
||||
hash, err := common.PasswordHash(request.NewPassword)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Model(&account).Update("password_hash", hash).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"changed": true})
|
||||
}
|
||||
|
||||
// CurrentGasAccount 解析当前账户和气站。
|
||||
func CurrentGasAccount(ctx *gin.Context) (models.GasAccount, models.GasBasic, bool) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return models.GasAccount{}, models.GasBasic{}, false
|
||||
}
|
||||
var account models.GasAccount
|
||||
if err := impl.DBService.Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
} else {
|
||||
infra.Response.Error(ctx, err)
|
||||
}
|
||||
return models.GasAccount{}, models.GasBasic{}, false
|
||||
}
|
||||
var station models.GasBasic
|
||||
if err := impl.DBService.First(&station, account.GasBasicID).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return models.GasAccount{}, models.GasBasic{}, false
|
||||
}
|
||||
return account, station, true
|
||||
}
|
||||
36
backend/api/internal/logic/gas/basic.go
Normal file
36
backend/api/internal/logic/gas/basic.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListMenu 返回 admin 角色的静态菜单。
|
||||
func ListMenu(ctx *gin.Context) {
|
||||
account, _, ok := CurrentGasAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
list := MenusForRole(account.RoleCode)
|
||||
infra.Response.Success(ctx, gin.H{"total": len(list), "list": list})
|
||||
}
|
||||
|
||||
// InvitationQRCode 返回根据当前气站 identity 动态生成的注册链接数据。
|
||||
func InvitationQRCode(ctx *gin.Context) {
|
||||
_, station, ok := CurrentGasAccount(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
registerURL, _ := url.Parse(config.Spec.Global.UserRegisterURL)
|
||||
query := registerURL.Query()
|
||||
query.Set("gas_identity", station.Identity)
|
||||
registerURL.RawQuery = query.Encode()
|
||||
infra.Response.Success(ctx, gin.H{
|
||||
"gas_basic_identity": station.Identity,
|
||||
"gas_basic_name": station.Name,
|
||||
"register_url": registerURL.String(),
|
||||
})
|
||||
}
|
||||
64
backend/api/internal/logic/gas/dashboard.go
Normal file
64
backend/api/internal/logic/gas/dashboard.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DashboardOverview(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
counts := gin.H{}
|
||||
queries := []struct {
|
||||
key string
|
||||
query any
|
||||
where string
|
||||
args []any
|
||||
}{
|
||||
{"delivery_count", &models.DeliveryBasic{}, "gas_basic_id = ? AND status <> ?", []any{station.ID, common.StatusArchived}},
|
||||
{"staff_count", &models.StaffAccount{}, "gas_basic_id = ? AND status <> ?", []any{station.ID, common.StatusArchived}},
|
||||
{"contract_count", &models.GasorderContract{}, "gas_basic_id = ? AND status <> ?", []any{station.ID, common.StatusArchived}},
|
||||
{"order_count", &models.GasorderBasic{}, "gas_basic_id = ? AND status <> ?", []any{station.ID, common.StatusArchived}},
|
||||
}
|
||||
for _, item := range queries {
|
||||
var count int64
|
||||
if err := impl.DBService.Model(item.query).Where(item.where, item.args...).Count(&count).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
counts[item.key] = count
|
||||
}
|
||||
var userCount int64
|
||||
if err := impl.DBService.Model(&models.UserServiceRelation{}).
|
||||
Where("gas_basic_id = ? AND status <> ?", station.ID, common.StatusArchived).Count(&userCount).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
counts["user_count"] = userCount
|
||||
infra.Response.Success(ctx, counts)
|
||||
}
|
||||
|
||||
func DashboardReport(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
type statusCount struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var orders []statusCount
|
||||
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||
Select("order_status AS status, count(*) AS count").
|
||||
Where("gas_basic_id = ? AND status <> ?", station.ID, common.StatusArchived).
|
||||
Group("order_status").Order("order_status").Scan(&orders).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"orders_by_status": orders})
|
||||
}
|
||||
136
backend/api/internal/logic/gas/delivery.go
Normal file
136
backend/api/internal/logic/gas/delivery.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListDelivery(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.DeliveryBasic
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(ctx,
|
||||
common.ActiveRecords(impl.DBService.Model(&models.DeliveryBasic{})).Where("gas_basic_id = ?", station.ID),
|
||||
&models.DeliveryBasic{})
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("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 GetDelivery(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondScopedRecord(ctx, common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND gas_basic_id = ?", ctx.Param("identity"), station.ID), &models.DeliveryBasic{})
|
||||
}
|
||||
|
||||
func CreateDelivery(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeliveryCode string `json:"delivery_code" binding:"required,max=32"`
|
||||
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
|
||||
}
|
||||
delivery := models.DeliveryBasic{
|
||||
Entity: common.NewEntity(common.StatusEnable), DeliveryCode: request.DeliveryCode, GasBasicID: station.ID,
|
||||
Name: request.Name, Principal: request.Principal, Address: request.Address,
|
||||
}
|
||||
if err := impl.DBService.Create(&delivery).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, delivery)
|
||||
}
|
||||
|
||||
func UpdateDelivery(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := requireDelivery(ctx, ctx.Param("identity"), station.ID); !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
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
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.DeliveryBasic{},
|
||||
gin.H{"name": request.Name, "principal": request.Principal, "address": request.Address},
|
||||
[]string{"name", "principal", "address"})
|
||||
}
|
||||
|
||||
func UpdateDeliveryStatus(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := requireDelivery(ctx, ctx.Param("identity"), station.ID); !ok {
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.DeliveryBasic{})
|
||||
}
|
||||
|
||||
func ArchiveDelivery(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delivery, ok := requireDelivery(ctx, ctx.Param("identity"), station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var blocking int64
|
||||
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||
Where("delivery_basic_id = ? AND order_status NOT IN ?", delivery.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.UserServiceRelation{}).
|
||||
Where("delivery_basic_id = ? AND status <> ?", delivery.ID, common.StatusArchived).Count(&blocking).Error; err != nil || blocking > 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&delivery).Update("status", common.StatusArchived).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&models.DeliveryAccount{}).
|
||||
Where("delivery_basic_id = ? AND status <> ?", delivery.ID, common.StatusArchived).
|
||||
Update("status", common.StatusArchived).Error
|
||||
}); err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
}
|
||||
140
backend/api/internal/logic/gas/delivery_account.go
Normal file
140
backend/api/internal/logic/gas/delivery_account.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ListDeliveryAccount(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.DeliveryAccount
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(ctx,
|
||||
common.ActiveRecords(impl.DBService.Model(&models.DeliveryAccount{})).
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_basic.gas_basic_id = ?", station.ID), &models.DeliveryAccount{})
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("delivery_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 GetDeliveryAccount(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.DeliveryAccount{})).
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_basic.gas_basic_id = ?", ctx.Param("identity"), station.ID)
|
||||
respondScopedRecord(ctx, query, &models.DeliveryAccount{})
|
||||
}
|
||||
|
||||
func CreateDeliveryAccount(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity" binding:"required"`
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !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
|
||||
}
|
||||
account := models.DeliveryAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), DeliveryBasicID: delivery.ID,
|
||||
Username: request.Username, PasswordHash: hash, DisplayName: request.DisplayName, RoleCode: "admin",
|
||||
}
|
||||
if err := impl.DBService.Create(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, account)
|
||||
}
|
||||
|
||||
func UpdateDeliveryAccount(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
if err := common.ActiveRecords(impl.DBService.Model(&models.DeliveryAccount{})).
|
||||
Select("delivery_account.*").
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_basic.gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
DisplayName string `json:"display_name" binding:"max=64"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.DeliveryAccount{}, gin.H{"display_name": request.DisplayName}, []string{"display_name"})
|
||||
}
|
||||
|
||||
func UpdateDeliveryAccountStatus(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var account models.DeliveryAccount
|
||||
if err := common.ActiveRecords(impl.DBService.Model(&models.DeliveryAccount{})).
|
||||
Select("delivery_account.*").
|
||||
Joins("JOIN delivery_basic ON delivery_basic.id = delivery_account.delivery_basic_id").
|
||||
Where("delivery_account.identity = ? AND delivery_basic.gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
First(&account).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.DeliveryAccount{})
|
||||
}
|
||||
|
||||
func ArchiveDeliveryAccount(ctx *gin.Context) {
|
||||
UpdateDeliveryAccountStatusWithValue(ctx, common.StatusArchived)
|
||||
}
|
||||
|
||||
func UpdateDeliveryAccountStatusWithValue(ctx *gin.Context, status int) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result := impl.DBService.Model(&models.DeliveryAccount{}).
|
||||
Where("identity = ? AND status <> ? AND delivery_basic_id IN (SELECT id FROM delivery_basic WHERE gas_basic_id = ?)",
|
||||
ctx.Param("identity"), common.StatusArchived, station.ID).
|
||||
Update("status", status)
|
||||
if result.Error != nil || result.RowsAffected != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
125
backend/api/internal/logic/gas/finance.go
Normal file
125
backend/api/internal/logic/gas/finance.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func currentWallet(ctx *gin.Context, gas models.GasBasic) (models.WalletBasic, bool) {
|
||||
var wallet models.WalletBasic
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("owner_type = ? AND owner_id = ? AND owner_identity = ?", "gas", gas.ID, gas.Identity).
|
||||
First(&wallet).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return wallet, false
|
||||
}
|
||||
return wallet, true
|
||||
}
|
||||
|
||||
func ListWalletBasic(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.WalletBasic{})).
|
||||
Where("owner_type = ? AND owner_id = ?", "gas", station.ID)
|
||||
listScoped(ctx, &models.WalletBasic{}, query, "wallet_basic.created_at desc")
|
||||
}
|
||||
|
||||
func listWalletChild(ctx *gin.Context, model any, table string) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(model)).
|
||||
Joins("JOIN wallet_basic ON wallet_basic.id = "+table+".wallet_basic_id").
|
||||
Where("wallet_basic.owner_type = ? AND wallet_basic.owner_id = ?", "gas", station.ID)
|
||||
listScoped(ctx, model, query, table+".created_at desc")
|
||||
}
|
||||
|
||||
func ListWalletBank(ctx *gin.Context) { listWalletChild(ctx, &models.WalletBank{}, "wallet_bank") }
|
||||
func ListWalletPayment(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletPayment{}, "wallet_payment")
|
||||
}
|
||||
func ListWalletRecord(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletRecord{}, "wallet_record")
|
||||
}
|
||||
func ListWalletRefund(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletRefund{}, "wallet_refund")
|
||||
}
|
||||
func ListWalletApplyCash(ctx *gin.Context) {
|
||||
listWalletChild(ctx, &models.WalletApplyCash{}, "wallet_apply_cash")
|
||||
}
|
||||
|
||||
func CreateWalletApplyCash(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
wallet, ok := currentWallet(ctx, station)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
WalletBankIdentity string `json:"wallet_bank_identity"`
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Channel string `json:"channel" binding:"required,max=32"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Amount <= 0 ||
|
||||
strings.TrimSpace(request.RequestNo) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var pendingAmount int64
|
||||
if err := impl.DBService.Model(&models.WalletApplyCash{}).
|
||||
Where("wallet_basic_id = ? AND status <> ? AND apply_status = ?", wallet.ID, common.StatusArchived, common.StatusPending).
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&pendingAmount).Error; err != nil ||
|
||||
request.Amount > wallet.WithdrawalBalance-pendingAmount {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var bankID uint64
|
||||
if request.WalletBankIdentity != "" {
|
||||
var bank models.WalletBank
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND wallet_basic_id = ?", request.WalletBankIdentity, wallet.ID).First(&bank).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
bankID = bank.ID
|
||||
}
|
||||
apply := models.WalletApplyCash{
|
||||
Entity: common.NewEntity(common.StatusEnable), ApplyStatus: common.StatusPending,
|
||||
WalletBasicID: wallet.ID, WalletBankID: bankID, CashNo: models.NewIdentity(),
|
||||
RequestNo: request.RequestNo, Amount: request.Amount, Channel: request.Channel, Remark: request.Remark,
|
||||
}
|
||||
if err := impl.DBService.Create(&apply).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, apply)
|
||||
}
|
||||
|
||||
func ListFinSettlement(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.FinSettlement{})).
|
||||
Where("subject_type = ? AND subject_id = ?", "gas", station.ID)
|
||||
listScoped(ctx, &models.FinSettlement{}, query, "fin_settlement.created_at desc")
|
||||
}
|
||||
|
||||
// 对账表目前没有主体归属字段,气站端不得暴露平台全局渠道数据。
|
||||
func ListFinReconciliation(ctx *gin.Context) {
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.FinReconciliation{})).Where("1 = 0")
|
||||
listScoped(ctx, &models.FinReconciliation{}, query, "fin_reconciliation.created_at desc")
|
||||
}
|
||||
281
backend/api/internal/logic/gas/gasorder.go
Normal file
281
backend/api/internal/logic/gas/gasorder.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"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"
|
||||
platformgasorder "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform/gasorder"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func rewriteJSON(ctx *gin.Context, mutate func(map[string]any) bool) bool {
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return false
|
||||
}
|
||||
var values map[string]any
|
||||
if err := json.Unmarshal(body, &values); err != nil || !mutate(values) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return false
|
||||
}
|
||||
body, err = json.Marshal(values)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return false
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
return true
|
||||
}
|
||||
|
||||
func scopedContract(ctx *gin.Context, identity string, gasID uint64) (models.GasorderContract, bool) {
|
||||
var contract models.GasorderContract
|
||||
if err := common.ActiveRecords(impl.DBService).Where("identity = ? AND gas_basic_id = ?", identity, gasID).First(&contract).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return contract, false
|
||||
}
|
||||
return contract, true
|
||||
}
|
||||
|
||||
func scopedOrder(ctx *gin.Context, identity string, gasID uint64) (models.GasorderBasic, bool) {
|
||||
var order models.GasorderBasic
|
||||
if err := common.ActiveRecords(impl.DBService).Where("identity = ? AND gas_basic_id = ?", identity, gasID).First(&order).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return order, false
|
||||
}
|
||||
return order, true
|
||||
}
|
||||
|
||||
func ListGasorderContract(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listScoped(ctx, &models.GasorderContract{}, common.ActiveRecords(impl.DBService.Model(&models.GasorderContract{})).
|
||||
Where("gas_basic_id = ?", station.ID), "gasorder_contract.created_at desc")
|
||||
}
|
||||
|
||||
func GetGasorderContract(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedContract(ctx, ctx.Param("identity"), station.ID); ok {
|
||||
platformgasorder.GetGasorderContract(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateGasorderContract(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
userIdentity, _ := values["user_account_identity"].(string)
|
||||
if _, _, valid := requireUser(ctx, userIdentity, station.ID); !valid {
|
||||
return false
|
||||
}
|
||||
if deliveryIdentity, _ := values["delivery_basic_identity"].(string); deliveryIdentity != "" {
|
||||
if _, valid := requireDelivery(ctx, deliveryIdentity, station.ID); !valid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
values["gas_basic_identity"] = station.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.CreateGasorderContract(ctx)
|
||||
}
|
||||
|
||||
func withContract(ctx *gin.Context, handler gin.HandlerFunc) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedContract(ctx, ctx.Param("identity"), station.ID); ok {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateGasorderContract(ctx *gin.Context) {
|
||||
withContract(ctx, platformgasorder.UpdateGasorderContract)
|
||||
}
|
||||
func ActivateGasorderContract(ctx *gin.Context) {
|
||||
withContract(ctx, platformgasorder.ActivateGasorderContract)
|
||||
}
|
||||
func RenewGasorderContract(ctx *gin.Context) {
|
||||
withContract(ctx, platformgasorder.RenewGasorderContract)
|
||||
}
|
||||
func TerminateGasorderContract(ctx *gin.Context) {
|
||||
withContract(ctx, platformgasorder.TerminateGasorderContract)
|
||||
}
|
||||
|
||||
func BindGasorderContractProduct(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
identity, _ := values["gasorder_contract_identity"].(string)
|
||||
_, valid := scopedContract(ctx, identity, station.ID)
|
||||
return valid
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.BindGasorderContractProduct(ctx)
|
||||
}
|
||||
|
||||
func UnbindGasorderContractProduct(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var binding models.GasorderContractProduct
|
||||
err := common.ActiveRecords(impl.DBService.Model(&models.GasorderContractProduct{})).
|
||||
Select("gasorder_contract_product.*").
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract_product.identity = ? AND gasorder_contract.gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
First(&binding).Error
|
||||
if err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
platformgasorder.UnbindGasorderContractProduct(ctx)
|
||||
}
|
||||
|
||||
func ListGasorderContractProduct(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.GasorderContractProduct{})).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_product.gasorder_contract_id").
|
||||
Where("gasorder_contract.gas_basic_id = ?", station.ID)
|
||||
listScoped(ctx, &models.GasorderContractProduct{}, query, "gasorder_contract_product.created_at desc")
|
||||
}
|
||||
|
||||
// ListContractProductCandidate 仅返回当前气站服务用户名下可用于合同绑定的气瓶。
|
||||
func ListContractProductCandidate(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.ProductInfo{})).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = product_info.user_account_id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("user_service_relation.gas_basic_id = ?", station.ID)
|
||||
listScoped(ctx, &models.ProductInfo{}, query, "product_info.created_at desc")
|
||||
}
|
||||
|
||||
func ListGasorderContractRevision(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.GasorderContractRevision{})).
|
||||
Joins("JOIN gasorder_contract ON gasorder_contract.id = gasorder_contract_revision.gasorder_contract_id").
|
||||
Where("gasorder_contract.gas_basic_id = ?", station.ID)
|
||||
listScoped(ctx, &models.GasorderContractRevision{}, query, "gasorder_contract_revision.created_at desc")
|
||||
}
|
||||
|
||||
func ListGasorderBasic(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listScoped(ctx, &models.GasorderBasic{}, common.ActiveRecords(impl.DBService.Model(&models.GasorderBasic{})).
|
||||
Where("gas_basic_id = ?", station.ID), "gasorder_basic.created_at desc")
|
||||
}
|
||||
|
||||
func GetGasorderBasic(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedOrder(ctx, ctx.Param("identity"), station.ID); ok {
|
||||
platformgasorder.GetGasorderBasic(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateGasorderBasic(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
contractIdentity, _ := values["gasorder_contract_identity"].(string)
|
||||
if _, valid := scopedContract(ctx, contractIdentity, station.ID); !valid {
|
||||
return false
|
||||
}
|
||||
values["creator_type"] = "gas"
|
||||
values["creator_identity"] = station.Identity
|
||||
return true
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.CreateGasorderBasic(ctx)
|
||||
}
|
||||
|
||||
func withOrder(ctx *gin.Context, handler gin.HandlerFunc) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedOrder(ctx, ctx.Param("identity"), station.ID); ok {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func AssignGasorderBasic(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := scopedOrder(ctx, ctx.Param("identity"), station.ID); !ok {
|
||||
return
|
||||
}
|
||||
if !rewriteJSON(ctx, func(values map[string]any) bool {
|
||||
deliveryIdentity, _ := values["delivery_basic_identity"].(string)
|
||||
staffIdentity, _ := values["staff_account_identity"].(string)
|
||||
_, deliveryOK := requireDelivery(ctx, deliveryIdentity, station.ID)
|
||||
_, staffOK := requireStaff(ctx, staffIdentity, station.ID)
|
||||
return deliveryOK && staffOK
|
||||
}) {
|
||||
return
|
||||
}
|
||||
platformgasorder.AssignGasorderBasic(ctx)
|
||||
}
|
||||
|
||||
func GasorderStartFilling(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderStartFilling) }
|
||||
func GasorderReady(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderReady) }
|
||||
func GasorderException(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderException) }
|
||||
func GasorderRecover(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderRecover) }
|
||||
func GasorderCancel(ctx *gin.Context) { withOrder(ctx, platformgasorder.GasorderCancel) }
|
||||
|
||||
func listScoped(ctx *gin.Context, model any, query *gorm.DB, order string) {
|
||||
page, size := common.PageSize(ctx)
|
||||
var total int64
|
||||
if err := common.ApplyKeywordFilter(ctx, query, model).Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
list := sliceForModel(model)
|
||||
if err := common.ApplyKeywordFilter(ctx, query, model).Order(order).Offset((page - 1) * size).Limit(size).Find(list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
respondList(ctx, list, total)
|
||||
}
|
||||
|
||||
func sliceForModel(model any) any {
|
||||
return reflect.New(reflect.SliceOf(reflect.TypeOf(model).Elem())).Interface()
|
||||
}
|
||||
56
backend/api/internal/logic/gas/menu.go
Normal file
56
backend/api/internal/logic/gas/menu.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package gas
|
||||
|
||||
import "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
|
||||
// Menu 是气站后台静态菜单定义。
|
||||
type Menu struct {
|
||||
Identity string `json:"identity"`
|
||||
ParentIdentity string `json:"parent_identity,omitempty"`
|
||||
GroupCode string `json:"group_code"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Path string `json:"path"`
|
||||
SortNo int `json:"sort_no"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
var adminMenus = []Menu{
|
||||
{Identity: "dashboard", GroupCode: "dashboard", Name: "数据概述", Icon: "icon-dashboard", Path: "/dashboard", SortNo: 10, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_overview", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "运营概览", Path: "/dashboard/overview", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "dashboard_reports", ParentIdentity: "dashboard", GroupCode: "dashboard", Name: "统计报表", Path: "/dashboard/reports", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "delivery", GroupCode: "delivery", Name: "配送点管理", Icon: "icon-storage", Path: "/delivery", SortNo: 20, Status: common.StatusEnable},
|
||||
{Identity: "delivery_basic", ParentIdentity: "delivery", GroupCode: "delivery", Name: "配送点列表", Path: "/delivery/points", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff", GroupCode: "staff", Name: "工作人员管理", Icon: "icon-user-group", Path: "/staff", SortNo: 30, Status: common.StatusEnable},
|
||||
{Identity: "staff_add", ParentIdentity: "staff", GroupCode: "staff", Name: "新增工作人员", Path: "/staff/add", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "staff_installer", ParentIdentity: "staff", GroupCode: "staff", Name: "安装人员", Path: "/staff/installers", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "staff_delivery", ParentIdentity: "staff", GroupCode: "staff", Name: "配送人员", Path: "/staff/delivery", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "staff_operations", ParentIdentity: "staff", GroupCode: "staff", Name: "运维人员", Path: "/staff/operations", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "user", GroupCode: "user", Name: "用户管理", Icon: "icon-user", Path: "/user", SortNo: 40, Status: common.StatusEnable},
|
||||
{Identity: "user_account", ParentIdentity: "user", GroupCode: "user", Name: "用户账户", Path: "/user/accounts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "contract", GroupCode: "contract", Name: "合同管理", Icon: "icon-file", Path: "/contract", SortNo: 50, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_contract", ParentIdentity: "contract", GroupCode: "contract", Name: "配送合同", Path: "/contract/contracts", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder", GroupCode: "gasorder", Name: "燃气配送订单", Icon: "icon-list", Path: "/gasorder", SortNo: 60, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_create", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "创建订单", Path: "/gasorder/create", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "gasorder_basic", ParentIdentity: "gasorder", GroupCode: "gasorder", Name: "配送订单", Path: "/gasorder/orders", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "finance", GroupCode: "finance", Name: "财务管理", Icon: "icon-bar-chart", Path: "/finance", SortNo: 70, Status: common.StatusEnable},
|
||||
{Identity: "wallet_basic", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包", Path: "/finance/wallet", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "wallet_bank", ParentIdentity: "finance", GroupCode: "finance", Name: "银行卡", Path: "/finance/banks", SortNo: 2, Status: common.StatusEnable},
|
||||
{Identity: "wallet_payment", ParentIdentity: "finance", GroupCode: "finance", Name: "支付记录", Path: "/finance/payments", SortNo: 3, Status: common.StatusEnable},
|
||||
{Identity: "wallet_record", ParentIdentity: "finance", GroupCode: "finance", Name: "钱包流水", Path: "/finance/records", SortNo: 4, Status: common.StatusEnable},
|
||||
{Identity: "wallet_refund", ParentIdentity: "finance", GroupCode: "finance", Name: "退款记录", Path: "/finance/refunds", SortNo: 5, Status: common.StatusEnable},
|
||||
{Identity: "wallet_apply_cash", ParentIdentity: "finance", GroupCode: "finance", Name: "提现申请", Path: "/finance/withdrawals", SortNo: 6, Status: common.StatusEnable},
|
||||
{Identity: "fin_settlement", ParentIdentity: "finance", GroupCode: "finance", Name: "财务结算", Path: "/finance/settlements", SortNo: 7, Status: common.StatusEnable},
|
||||
{Identity: "fin_reconciliation", ParentIdentity: "finance", GroupCode: "finance", Name: "财务对账", Path: "/finance/reconciliations", SortNo: 8, Status: common.StatusEnable},
|
||||
{Identity: "ticket", GroupCode: "ticket", Name: "工单管理", Icon: "icon-customer-service", Path: "/ticket", SortNo: 80, Status: common.StatusEnable},
|
||||
{Identity: "cs_ticket", ParentIdentity: "ticket", GroupCode: "ticket", Name: "客服工单", Path: "/ticket/tickets", SortNo: 1, Status: common.StatusEnable},
|
||||
{Identity: "invitation", GroupCode: "invitation", Name: "邀请注册", Icon: "icon-qrcode", Path: "/invitation", SortNo: 90, Status: common.StatusEnable},
|
||||
{Identity: "invitation_qrcode", ParentIdentity: "invitation", GroupCode: "invitation", Name: "邀请二维码", Path: "/invitation/qrcode", SortNo: 1, Status: common.StatusEnable},
|
||||
}
|
||||
|
||||
// MenusForRole 返回角色允许的菜单。
|
||||
func MenusForRole(roleCode string) []Menu {
|
||||
if roleCode != "admin" {
|
||||
return nil
|
||||
}
|
||||
return append([]Menu(nil), adminMenus...)
|
||||
}
|
||||
26
backend/api/internal/logic/gas/menu_test.go
Normal file
26
backend/api/internal/logic/gas/menu_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package gas
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMenusOnlyAllowGasAdmin(t *testing.T) {
|
||||
if got := MenusForRole("admin"); len(got) == 0 {
|
||||
t.Fatal("admin must receive gas menus")
|
||||
}
|
||||
for _, role := range []string{"root", "delivery", "installer", "operations", ""} {
|
||||
if got := MenusForRole(role); len(got) != 0 {
|
||||
t.Fatalf("role %q must not receive gas admin menus", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMenuContainsContractAndDeliveryManagement(t *testing.T) {
|
||||
found := map[string]bool{}
|
||||
for _, menu := range MenusForRole("admin") {
|
||||
found[menu.Identity] = true
|
||||
}
|
||||
for _, identity := range []string{"delivery_basic", "gasorder_contract", "invitation_qrcode"} {
|
||||
if !found[identity] {
|
||||
t.Fatalf("missing confirmed menu %q", identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
backend/api/internal/logic/gas/resource_contract.go
Normal file
45
backend/api/internal/logic/gas/resource_contract.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package gas
|
||||
|
||||
type ResourceMode string
|
||||
|
||||
const (
|
||||
Writable ResourceMode = "writable"
|
||||
ReadOnly ResourceMode = "readonly"
|
||||
AppendOnly ResourceMode = "append_only"
|
||||
Managed ResourceMode = "managed"
|
||||
)
|
||||
|
||||
type ResourceContract struct {
|
||||
Domain string `json:"domain"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
PageKind string `json:"pageKind"`
|
||||
Mode ResourceMode `json:"mode"`
|
||||
}
|
||||
|
||||
func ExpectedResources() []ResourceContract {
|
||||
items := []struct {
|
||||
domain string
|
||||
name string
|
||||
mode ResourceMode
|
||||
}{
|
||||
{"delivery", "delivery_basic", Writable}, {"delivery", "delivery_account", Writable},
|
||||
{"staff", "staff_account", Writable}, {"staff", "staff_credential", Writable},
|
||||
{"user", "user_account", Writable}, {"user", "user_address", Writable},
|
||||
{"contract", "gasorder_contract", Managed}, {"contract", "gasorder_contract_product", AppendOnly},
|
||||
{"contract", "gasorder_contract_revision", ReadOnly}, {"gasorder", "gasorder_basic", AppendOnly},
|
||||
{"contract", "product_info", ReadOnly},
|
||||
{"finance", "wallet_basic", ReadOnly}, {"finance", "wallet_bank", ReadOnly},
|
||||
{"finance", "wallet_payment", ReadOnly}, {"finance", "wallet_record", ReadOnly},
|
||||
{"finance", "wallet_refund", ReadOnly}, {"finance", "wallet_apply_cash", AppendOnly},
|
||||
{"finance", "fin_settlement", ReadOnly}, {"finance", "fin_reconciliation", ReadOnly},
|
||||
{"ticket", "cs_ticket", Writable},
|
||||
}
|
||||
result := make([]ResourceContract, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, ResourceContract{
|
||||
Domain: item.domain, Name: item.name, Path: "/" + item.name, PageKind: "list", Mode: item.mode,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
79
backend/api/internal/logic/gas/scope.go
Normal file
79
backend/api/internal/logic/gas/scope.go
Normal file
@@ -0,0 +1,79 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func currentGas(ctx *gin.Context) (models.GasBasic, bool) {
|
||||
_, station, ok := CurrentGasAccount(ctx)
|
||||
return station, ok
|
||||
}
|
||||
|
||||
func respondList(ctx *gin.Context, list any, total int64) {
|
||||
response, err := common.PublicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": response})
|
||||
}
|
||||
|
||||
func respondScopedRecord(ctx *gin.Context, query *gorm.DB, model any) {
|
||||
if err := query.First(model).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := common.PublicResourceResponse(model)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
func requireDelivery(ctx *gin.Context, identity string, gasID uint64) (models.DeliveryBasic, bool) {
|
||||
var delivery models.DeliveryBasic
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND gas_basic_id = ?", identity, gasID).First(&delivery).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return models.DeliveryBasic{}, false
|
||||
}
|
||||
return delivery, true
|
||||
}
|
||||
|
||||
func requireStaff(ctx *gin.Context, identity string, gasID uint64) (models.StaffAccount, bool) {
|
||||
var staff models.StaffAccount
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND gas_basic_id = ?", identity, gasID).First(&staff).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return models.StaffAccount{}, false
|
||||
}
|
||||
return staff, true
|
||||
}
|
||||
|
||||
func requireUser(ctx *gin.Context, identity string, gasID uint64) (models.UserAccount, models.UserServiceRelation, bool) {
|
||||
var user models.UserAccount
|
||||
var relation models.UserServiceRelation
|
||||
err := common.ActiveRecords(impl.DBService.Model(&models.UserAccount{})).
|
||||
Select("user_account.*").
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = user_account.id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("user_account.identity = ? AND user_service_relation.gas_basic_id = ?", identity, gasID).
|
||||
First(&user).Error
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return models.UserAccount{}, models.UserServiceRelation{}, false
|
||||
}
|
||||
if err := common.ActiveRecords(impl.DBService).
|
||||
Where("user_account_id = ? AND gas_basic_id = ?", user.ID, gasID).First(&relation).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return models.UserAccount{}, models.UserServiceRelation{}, false
|
||||
}
|
||||
return user, relation, true
|
||||
}
|
||||
287
backend/api/internal/logic/gas/staff.go
Normal file
287
backend/api/internal/logic/gas/staff.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package gas
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var gasStaffRoles = map[string]bool{"installer": true, "delivery": true, "operations": true}
|
||||
var gasWorkStatuses = map[string]bool{"on_duty": true, "off_duty": true}
|
||||
|
||||
func ListStaff(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.StaffAccount
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(ctx,
|
||||
common.ActiveRecords(impl.DBService.Model(&models.StaffAccount{})).Where("gas_basic_id = ?", station.ID),
|
||||
&models.StaffAccount{})
|
||||
if roleCode := ctx.Query("role_code"); roleCode != "" {
|
||||
if !gasStaffRoles[roleCode] {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
query = query.Where("role_code = ?", roleCode)
|
||||
}
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("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 GetStaff(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
respondScopedRecord(ctx, common.ActiveRecords(impl.DBService).
|
||||
Where("identity = ? AND gas_basic_id = ?", ctx.Param("identity"), station.ID), &models.StaffAccount{})
|
||||
}
|
||||
|
||||
type staffRequest 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:"max=512"`
|
||||
RoleCode string `json:"role_code" binding:"required"`
|
||||
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
|
||||
WorkStatus string `json:"work_status" binding:"required"`
|
||||
}
|
||||
|
||||
func CreateStaff(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || request.Username == "" ||
|
||||
!common.IsValidAccountPassword(request.Password) || !gasStaffRoles[request.RoleCode] || !gasWorkStatuses[request.WorkStatus] {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var deliveryID uint64
|
||||
if request.DeliveryBasicIdentity != "" {
|
||||
delivery, ok := requireDelivery(ctx, request.DeliveryBasicIdentity, station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deliveryID = delivery.ID
|
||||
}
|
||||
hash, err := common.PasswordHash(request.Password)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
staff := models.StaffAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.Avatar, RoleCode: request.RoleCode,
|
||||
GasBasicID: station.ID, DeliveryBasicID: deliveryID, WorkStatus: request.WorkStatus,
|
||||
}
|
||||
if err := impl.DBService.Create(&staff).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, staff)
|
||||
}
|
||||
|
||||
func UpdateStaff(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := requireStaff(ctx, ctx.Param("identity"), station.ID); !ok {
|
||||
return
|
||||
}
|
||||
var request staffRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || !gasStaffRoles[request.RoleCode] || !gasWorkStatuses[request.WorkStatus] {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var deliveryID uint64
|
||||
if request.DeliveryBasicIdentity != "" {
|
||||
delivery, ok := requireDelivery(ctx, request.DeliveryBasicIdentity, station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deliveryID = delivery.ID
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.StaffAccount{}, gin.H{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "role_code": request.RoleCode,
|
||||
"delivery_basic_id": deliveryID, "work_status": request.WorkStatus,
|
||||
}, []string{"name", "phone", "avatar", "role_code", "delivery_basic_id", "work_status"})
|
||||
}
|
||||
|
||||
func UpdateStaffStatus(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, ok := requireStaff(ctx, ctx.Param("identity"), station.ID); !ok {
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.StaffAccount{})
|
||||
}
|
||||
|
||||
func ArchiveStaff(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
staff, ok := requireStaff(ctx, ctx.Param("identity"), station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var blocking int64
|
||||
if err := impl.DBService.Model(&models.GasorderBasic{}).
|
||||
Where("staff_account_id = ? AND order_status NOT IN ?", staff.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(&staff).Update("status", common.StatusArchived).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
}
|
||||
|
||||
func ListCredential(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.StaffCredential
|
||||
var total int64
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.StaffCredential{})).
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_account.gas_basic_id = ?", station.ID)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("staff_credential.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 GetCredential(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
query := common.ActiveRecords(impl.DBService.Model(&models.StaffCredential{})).
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_credential.identity = ? AND staff_account.gas_basic_id = ?", ctx.Param("identity"), station.ID)
|
||||
respondScopedRecord(ctx, query, &models.StaffCredential{})
|
||||
}
|
||||
|
||||
type credentialRequest struct {
|
||||
StaffAccountIdentity string `json:"staff_account_identity" binding:"required"`
|
||||
CredentialType string `json:"credential_type" binding:"required,max=64"`
|
||||
CredentialNo string `json:"credential_no" binding:"max=128"`
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
func CreateCredential(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := requireStaff(ctx, request.StaffAccountIdentity, station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
credential := models.StaffCredential{
|
||||
Entity: common.NewEntity(common.StatusEnable), StaffAccountID: staff.ID,
|
||||
CredentialType: request.CredentialType, CredentialNo: request.CredentialNo, ExpiredAt: request.ExpiredAt,
|
||||
}
|
||||
if err := impl.DBService.Create(&credential).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, credential)
|
||||
}
|
||||
|
||||
func UpdateCredential(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var existing models.StaffCredential
|
||||
if err := common.ActiveRecords(impl.DBService.Model(&models.StaffCredential{})).
|
||||
Select("staff_credential.*").
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_credential.identity = ? AND staff_account.gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
First(&existing).Error; err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
var request credentialRequest
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
staff, ok := requireStaff(ctx, request.StaffAccountIdentity, station.ID)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
common.UpdateAllowedByIdentity(ctx, &models.StaffCredential{}, gin.H{
|
||||
"staff_account_id": staff.ID, "credential_type": request.CredentialType,
|
||||
"credential_no": request.CredentialNo, "expired_at": request.ExpiredAt,
|
||||
}, []string{"staff_account_id", "credential_type", "credential_no", "expired_at"})
|
||||
}
|
||||
|
||||
func UpdateCredentialStatus(ctx *gin.Context) {
|
||||
archiveCredentialWithStatus(ctx, 0, false)
|
||||
}
|
||||
|
||||
func ArchiveCredential(ctx *gin.Context) {
|
||||
archiveCredentialWithStatus(ctx, common.StatusArchived, true)
|
||||
}
|
||||
|
||||
func archiveCredentialWithStatus(ctx *gin.Context, status int, fixed bool) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := impl.DBService.Model(&models.StaffCredential{}).
|
||||
Joins("JOIN staff_account ON staff_account.id = staff_credential.staff_account_id").
|
||||
Where("staff_credential.identity = ? AND staff_account.gas_basic_id = ?", ctx.Param("identity"), station.ID).
|
||||
Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if fixed {
|
||||
if err := impl.DBService.Model(&models.StaffCredential{}).Where("identity = ?", ctx.Param("identity")).Update("status", status).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"archived": true})
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.StaffCredential{})
|
||||
}
|
||||
121
backend/api/internal/logic/gas/ticket.go
Normal file
121
backend/api/internal/logic/gas/ticket.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ticketQuery(gasID uint64) *gorm.DB {
|
||||
return common.ActiveRecords(impl.DBService.Model(&models.CsTicket{})).
|
||||
Joins("JOIN user_service_relation ON user_service_relation.user_account_id = cs_ticket.user_account_id AND user_service_relation.status <> ?",
|
||||
common.StatusArchived).
|
||||
Where("user_service_relation.gas_basic_id = ?", gasID)
|
||||
}
|
||||
|
||||
func ListTicket(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
listScoped(ctx, &models.CsTicket{}, ticketQuery(station.ID), "cs_ticket.created_at desc")
|
||||
}
|
||||
|
||||
func GetTicket(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var ticket models.CsTicket
|
||||
respondScopedRecord(ctx, ticketQuery(station.ID).Where("cs_ticket.identity = ?", ctx.Param("identity")), &ticket)
|
||||
}
|
||||
|
||||
type ticketRequest struct {
|
||||
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
|
||||
TicketNo string `json:"ticket_no" binding:"required,max=64"`
|
||||
Category string `json:"category" binding:"required,max=64"`
|
||||
Priority string `json:"priority" binding:"required,max=16"`
|
||||
}
|
||||
|
||||
func CreateTicket(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var request ticketRequest
|
||||
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
|
||||
}
|
||||
ticket := models.CsTicket{
|
||||
Entity: common.NewEntity(common.StatusEnable), TicketStatus: common.StatusOpen,
|
||||
TicketNo: request.TicketNo, UserAccountID: user.ID, Category: request.Category, Priority: request.Priority,
|
||||
}
|
||||
if err := impl.DBService.Create(&ticket).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
common.RespondCreatedResource(ctx, ticket)
|
||||
}
|
||||
|
||||
func UpdateTicket(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var ticket models.CsTicket
|
||||
if err := ticketQuery(station.ID).Where("cs_ticket.identity = ?", ctx.Param("identity")).First(&ticket).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var request ticketRequest
|
||||
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
|
||||
}
|
||||
if err := impl.DBService.Model(&ticket).Updates(map[string]any{
|
||||
"user_account_id": user.ID, "ticket_no": request.TicketNo, "category": request.Category, "priority": request.Priority,
|
||||
}).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
func UpdateTicketStatus(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := ticketQuery(station.ID).Where("cs_ticket.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.UpdateRecordStatus(ctx, &models.CsTicket{})
|
||||
}
|
||||
|
||||
func ArchiveTicket(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := ticketQuery(station.ID).Where("cs_ticket.identity = ?", ctx.Param("identity")).Count(&count).Error; err != nil || count != 1 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
common.ArchiveRecord(ctx, &models.CsTicket{})
|
||||
}
|
||||
355
backend/api/internal/logic/gas/user.go
Normal file
355
backend/api/internal/logic/gas/user.go
Normal file
@@ -0,0 +1,355 @@
|
||||
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/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListUser(ctx *gin.Context) {
|
||||
station, ok := currentGas(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
page, size := common.PageSize(ctx)
|
||||
var list []models.UserAccount
|
||||
var total int64
|
||||
query := common.ApplyKeywordFilter(ctx,
|
||||
common.ActiveRecords(impl.DBService.Model(&models.UserAccount{})).
|
||||
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 = ?", 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, ok := requireUser(ctx, ctx.Param("identity"), station.ID)
|
||||
if !ok {
|
||||
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)
|
||||
}
|
||||
|
||||
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:"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
|
||||
}
|
||||
user := models.UserAccount{
|
||||
Entity: common.NewEntity(common.StatusEnable), Username: request.Username, PasswordHash: hash,
|
||||
Name: request.Name, Phone: request.Phone, Avatar: request.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 {
|
||||
if err := tx.Model(&user).Updates(map[string]any{
|
||||
"name": request.Name, "phone": request.Phone, "avatar": request.Avatar, "real_name": request.RealName,
|
||||
}).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
|
||||
}
|
||||
Reference in New Issue
Block a user