Files
EasyGoAdmin_GoFrame_EleVue/internal/logic/city.go
T
2026-10-20 11:37:25 +08:00

305 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"strings"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"easygoadmin/api/v1"
"easygoadmin/internal/consts"
"easygoadmin/internal/dao"
"easygoadmin/internal/service"
)
// ============================== 城市业务实现 ==============================
// sCity 城市业务实现
// 实现 service.ICity 接口,承载城市模块的业务逻辑
type sCity struct{}
// citySerializeMaps 枚举字段到字典编码的映射
// {field}Text 由 dict_item 解析,字典管理页可修改文案
var citySerializeMaps = map[string]string{
"level": "city_level",
}
func init() {
service.RegisterCity(&sCity{})
}
// citySpec 城市模块声明
//
// 仅 Page 采用声明式;List 含 parentCode、limit 等特有查询,Detail 使用嵌入结构,均手写实现
var citySpec = ModuleSpec[v1.CityItem, v1.CityItem, v1.CityDetailRes]{
Model: dao.City.Ctx,
LikeFields: []string{"name", "pinyin"},
EqFields: []string{"level"},
OrderBy: "id",
SerializeMaps: citySerializeMaps,
ToData: cityToData,
}
// cityToData 全量表单语义转换
// 兼容新增与修改请求,将入参转换为落库字段
func cityToData(req any) map[string]any {
switch r := req.(type) {
case *v1.CityAddReq:
return map[string]any{
"city_code": r.CityCode, "area_code": r.AreaCode, "parent_code": r.ParentCode,
"zip_code": r.ZipCode, "level": r.Level, "pid": r.Pid, "name": r.Name,
"short_name": r.ShortName, "full_name": r.FullName, "pinyin": r.Pinyin,
"lng": r.Lng, "lat": r.Lat,
}
case *v1.CityUpdateReq:
return map[string]any{
"city_code": r.CityCode, "area_code": r.AreaCode, "parent_code": r.ParentCode,
"zip_code": r.ZipCode, "level": r.Level, "pid": r.Pid, "name": r.Name,
"short_name": r.ShortName, "full_name": r.FullName, "pinyin": r.Pinyin,
"lng": r.Lng, "lat": r.Lat,
}
}
panic("cityToData: 不支持的入参类型")
}
// Page 城市分页
// 按查询条件分页返回城市列表,采用声明式实现
func (s *sCity) Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error) {
list, total, err := citySpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
if err != nil {
return nil, err
}
return v1.NewPageData(req.GetPageNo(), req.GetPageSize(), total, derefSlice(list)), nil
}
// List 城市列表
// 按父级编码、层级、关键词等条件返回城市列表,供级联选择器使用
func (s *sCity) List(ctx context.Context, req *v1.CityListReq) (res v1.CityListRes, err error) {
m := dao.City.Ctx(ctx)
if req.ParentCode != "" {
m = m.Where("parent_code", req.ParentCode)
} else if req.Level > 0 {
m = m.Where("level", req.Level)
}
if req.Keyword != "" {
kw := "%" + req.Keyword + "%"
m = m.WhereLike("name", kw).WhereOrLike("pinyin", kw)
}
if req.Limit > 0 {
m = m.Limit(req.Limit)
}
rows, err := m.Order("id").All()
if err != nil {
return nil, err
}
list := make([]v1.CityItem, 0, len(rows))
for _, r := range rows {
list = append(list, s.toItem(ctx, r))
}
return list, nil
}
// Detail 城市详情
// 按城市ID查询详情,并补充父级名称
func (s *sCity) Detail(ctx context.Context, req *v1.CityDetailReq) (res *v1.CityDetailRes, err error) {
one, err := dao.City.Ctx(ctx).Where("id", req.Id).One()
if err != nil {
return nil, err
}
if len(one) == 0 {
return nil, gerror.NewCode(consts.CodeRecordNotFound)
}
res = &v1.CityDetailRes{CityItem: s.toItem(ctx, one)}
if pid := one["pid"].Int64(); pid > 0 {
if parent, e := dao.City.Ctx(ctx).Where("id", pid).Fields("name").One(); e == nil && len(parent) > 0 {
res.ParentName = parent["name"].String()
}
}
return res, nil
}
// Add 新增城市
// 新增成功后返回新记录ID,采用声明式实现
func (s *sCity) Add(ctx context.Context, req *v1.CityAddReq) (res *v1.IdRes, err error) {
id, err := citySpec.Add(ctx, req)
if err != nil {
return nil, err
}
return &v1.IdRes{Id: id}, nil
}
// Update 修改城市
// 按城市ID更新记录,采用声明式实现
func (s *sCity) Update(ctx context.Context, req *v1.CityUpdateReq) (res *v1.CityUpdateRes, err error) {
if err = citySpec.Update(ctx, req.Id, req); err != nil {
return nil, err
}
return &v1.CityUpdateRes{}, nil
}
// Delete 删除城市
// 按城市ID删除单条记录,采用声明式实现
func (s *sCity) Delete(ctx context.Context, req *v1.CityDeleteReq) (res *v1.CityDeleteRes, err error) {
if err = citySpec.Delete(ctx, req.Id); err != nil {
return nil, err
}
return &v1.CityDeleteRes{}, nil
}
// BatchDelete 批量删除城市
// 按城市ID列表批量删除记录,采用声明式实现
func (s *sCity) BatchDelete(ctx context.Context, req *v1.CityBatchDeleteReq) (res *v1.CityBatchDeleteRes, err error) {
if err = citySpec.BatchDelete(ctx, req.Ids); err != nil {
return nil, err
}
return &v1.CityBatchDeleteRes{}, nil
}
// ByParent 按父编码查询下级城市
// 登录用户即可访问,返回指定父级下的直属下级城市
func (s *sCity) ByParent(ctx context.Context, req *v1.CityByParentReq) (res *v1.CityByParentRes, err error) {
rows, err := dao.City.Ctx(ctx).Where("parent_code", req.ParentCode).Order("id").All()
if err != nil {
return nil, err
}
list := make([]v1.CityItem, 0, len(rows))
for _, r := range rows {
list = append(list, s.toItem(ctx, r))
}
return &v1.CityByParentRes{List: list}, nil
}
// GetCityList 按父编码获取下级城市
// 登录用户即可访问,供级联选择器懒加载;parentCode 为 0 时返回顶级
func (s *sCity) GetCityList(ctx context.Context, req *v1.CityGetListReq) (res v1.CityGetListRes, err error) {
rows, err := dao.City.Ctx(ctx).Where("parent_code", req.ParentCode).Order("id").All()
if err != nil {
return nil, err
}
list := make([]v1.CityItem, 0, len(rows))
for _, r := range rows {
list = append(list, s.toItem(ctx, r))
}
return list, nil
}
// AreaProvince 省份列表
// 登录用户即可访问,返回层级为省的顶级城市列表
func (s *sCity) AreaProvince(ctx context.Context, req *v1.AreaParentReq) (res *v1.AreaParentRes, err error) {
rows, err := dao.City.Ctx(ctx).Where("level", 1).Order("id").All()
if err != nil {
return nil, err
}
list := make([]v1.CityItem, 0, len(rows))
for _, r := range rows {
list = append(list, s.toItem(ctx, r))
}
return &v1.AreaParentRes{List: list}, nil
}
// ============================================================
// 城市编解码(名称 ↔ 编码互转)
// ============================================================
// ResolveCityNames 城市编码数组转名称串
//
// 按 area_code 查询 name,保留输入顺序,空编码跳过。
// 例:["320000000000","320100000000","320105000000"] 转换为 "江苏省,南京市,建邺区"
func ResolveCityNames(ctx context.Context, codes []string, separator string) string {
clean := make([]string, 0, len(codes))
for _, c := range codes {
if c != "" {
clean = append(clean, c)
}
}
if len(clean) == 0 {
return ""
}
rows, err := dao.City.Ctx(ctx).WhereIn("area_code", clean).Fields("area_code, name").All()
if err != nil {
return ""
}
nameByCode := make(map[string]string, len(rows))
for _, r := range rows {
nameByCode[r["area_code"].String()] = r["name"].String()
}
names := make([]string, 0, len(clean))
for _, code := range clean {
if n := nameByCode[code]; n != "" {
names = append(names, n)
}
}
if separator == "" {
separator = ","
}
return strings.Join(names, separator)
}
// ResolveCityCodes 名称串转四级编码数组
//
// 逐级以 parent 约束避免同名歧义。
// 例:"江苏省,南京市,建邺区" 转换为 ["320000000000","320100000000","320105000000",""]
func ResolveCityCodes(ctx context.Context, cityInfo string) [4]string {
var codes [4]string
cityInfo = strings.TrimSpace(cityInfo)
if cityInfo == "" {
return codes
}
names := make([]string, 0, 4)
for _, part := range strings.Split(cityInfo, ",") {
if p := strings.TrimSpace(part); p != "" {
names = append(names, p)
}
}
if len(names) == 0 {
return codes
}
// 第一级:省(pid = 0)
prov, err := dao.City.Ctx(ctx).Where("pid", 0).Where("name", names[0]).One()
if err != nil || len(prov) == 0 {
return codes
}
codes[0] = prov["area_code"].String()
// 第二级:市(parent_code = 省 area_code)
if len(names) > 1 {
if city, e := dao.City.Ctx(ctx).Where("parent_code", codes[0]).Where("name", names[1]).One(); e == nil && len(city) > 0 {
codes[1] = city["area_code"].String()
}
}
// 第三级:区(parent_code = 市 area_code)
if len(names) > 2 && codes[1] != "" {
if dist, e := dao.City.Ctx(ctx).Where("parent_code", codes[1]).Where("name", names[2]).One(); e == nil && len(dist) > 0 {
codes[2] = dist["area_code"].String()
}
}
// 第四级:街道(parent_code = 区 area_code)
if len(names) > 3 && codes[2] != "" {
if street, e := dao.City.Ctx(ctx).Where("parent_code", codes[2]).Where("name", names[3]).One(); e == nil && len(street) > 0 {
codes[3] = street["area_code"].String()
}
}
return codes
}
// toItem 数据库记录转换为城市行数据
// 枚举层级同步补全层级文本
func (s *sCity) toItem(ctx context.Context, r gdb.Record) v1.CityItem {
return v1.CityItem{
Id: r["id"].Int64(),
CityCode: r["city_code"].String(),
AreaCode: r["area_code"].String(),
ParentCode: r["parent_code"].String(),
ZipCode: r["zip_code"].String(),
Level: r["level"].Int(),
LevelText: DictGetText(ctx, citySerializeMaps["level"], r["level"].Int()),
Pid: r["pid"].Int64(),
Name: r["name"].String(),
ShortName: r["short_name"].String(),
FullName: r["full_name"].String(),
Pinyin: r["pinyin"].String(),
Lng: r["lng"].String(),
Lat: r["lat"].String(),
}
}