feat: implement gas and delivery admin systems

This commit is contained in:
david
2026-07-30 14:16:58 +08:00
parent 1093385f95
commit f5ecc0d973
252 changed files with 49385 additions and 363 deletions

View 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})
}

View 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 }

View 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})
}

View 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)
}

View 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...)
}

View 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)
}
}
}

View 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})
}

View 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
}

View 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{})
}

View 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{})
}
}

View 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
})
}

View 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
}

View 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(),
})
}

View 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})
}

View 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})
}

View 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})
}

View 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")
}

View 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()
}

View 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...)
}

View 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)
}
}
}

View 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
}

View 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
}

View 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{})
}

View 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{})
}

View 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
}