表情包
This commit is contained in:
parent
a5475f9957
commit
b8ead156e6
@ -5,3 +5,14 @@ dist
|
|||||||
bin
|
bin
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# These files do not participate in Go container builds. Keeping them out of the
|
||||||
|
# build context prevents documentation/deploy-only edits from invalidating COPY . .
|
||||||
|
AGENTS.md
|
||||||
|
README.md
|
||||||
|
go.work
|
||||||
|
docs
|
||||||
|
deploy
|
||||||
|
scripts
|
||||||
|
tools
|
||||||
|
server/admin/README.md
|
||||||
|
server/admin/docs
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -566,6 +566,21 @@ message ListRegistrationCountriesResponse {
|
|||||||
repeated Country countries = 1;
|
repeated Country countries = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LoginRiskBlockedCountry {
|
||||||
|
string country_code = 1;
|
||||||
|
string keyword = 2;
|
||||||
|
int64 updated_at_ms = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListLoginRiskBlockedCountriesRequest {
|
||||||
|
RequestMeta meta = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListLoginRiskBlockedCountriesResponse {
|
||||||
|
repeated LoginRiskBlockedCountry countries = 1;
|
||||||
|
int64 updated_at_ms = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ListRegionsRequest {
|
message ListRegionsRequest {
|
||||||
RequestMeta meta = 1;
|
RequestMeta meta = 1;
|
||||||
string status = 2;
|
string status = 2;
|
||||||
@ -731,6 +746,7 @@ service CountryAdminService {
|
|||||||
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
||||||
service CountryQueryService {
|
service CountryQueryService {
|
||||||
rpc ListRegistrationCountries(ListRegistrationCountriesRequest) returns (ListRegistrationCountriesResponse);
|
rpc ListRegistrationCountries(ListRegistrationCountriesRequest) returns (ListRegistrationCountriesResponse);
|
||||||
|
rpc ListLoginRiskBlockedCountries(ListLoginRiskBlockedCountriesRequest) returns (ListLoginRiskBlockedCountriesResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegionAdminService 只保留 App 后端需要承接的区域查询、展示字段更新和国家归属替换 RPC。
|
// RegionAdminService 只保留 App 后端需要承接的区域查询、展示字段更新和国家归属替换 RPC。
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// - protoc-gen-go-grpc v1.5.1
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
// - protoc v4.25.3
|
// - protoc v5.28.3
|
||||||
// source: proto/user/v1/user.proto
|
// source: proto/user/v1/user.proto
|
||||||
|
|
||||||
package userv1
|
package userv1
|
||||||
@ -1493,7 +1493,8 @@ var CountryAdminService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CountryQueryService_ListRegistrationCountries_FullMethodName = "/hyapp.user.v1.CountryQueryService/ListRegistrationCountries"
|
CountryQueryService_ListRegistrationCountries_FullMethodName = "/hyapp.user.v1.CountryQueryService/ListRegistrationCountries"
|
||||||
|
CountryQueryService_ListLoginRiskBlockedCountries_FullMethodName = "/hyapp.user.v1.CountryQueryService/ListLoginRiskBlockedCountries"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CountryQueryServiceClient is the client API for CountryQueryService service.
|
// CountryQueryServiceClient is the client API for CountryQueryService service.
|
||||||
@ -1503,6 +1504,7 @@ const (
|
|||||||
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
||||||
type CountryQueryServiceClient interface {
|
type CountryQueryServiceClient interface {
|
||||||
ListRegistrationCountries(ctx context.Context, in *ListRegistrationCountriesRequest, opts ...grpc.CallOption) (*ListRegistrationCountriesResponse, error)
|
ListRegistrationCountries(ctx context.Context, in *ListRegistrationCountriesRequest, opts ...grpc.CallOption) (*ListRegistrationCountriesResponse, error)
|
||||||
|
ListLoginRiskBlockedCountries(ctx context.Context, in *ListLoginRiskBlockedCountriesRequest, opts ...grpc.CallOption) (*ListLoginRiskBlockedCountriesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type countryQueryServiceClient struct {
|
type countryQueryServiceClient struct {
|
||||||
@ -1523,6 +1525,16 @@ func (c *countryQueryServiceClient) ListRegistrationCountries(ctx context.Contex
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *countryQueryServiceClient) ListLoginRiskBlockedCountries(ctx context.Context, in *ListLoginRiskBlockedCountriesRequest, opts ...grpc.CallOption) (*ListLoginRiskBlockedCountriesResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ListLoginRiskBlockedCountriesResponse)
|
||||||
|
err := c.cc.Invoke(ctx, CountryQueryService_ListLoginRiskBlockedCountries_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CountryQueryServiceServer is the server API for CountryQueryService service.
|
// CountryQueryServiceServer is the server API for CountryQueryService service.
|
||||||
// All implementations must embed UnimplementedCountryQueryServiceServer
|
// All implementations must embed UnimplementedCountryQueryServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@ -1530,6 +1542,7 @@ func (c *countryQueryServiceClient) ListRegistrationCountries(ctx context.Contex
|
|||||||
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
// CountryQueryService 承载 App 注册页公开国家列表查询。
|
||||||
type CountryQueryServiceServer interface {
|
type CountryQueryServiceServer interface {
|
||||||
ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error)
|
ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error)
|
||||||
|
ListLoginRiskBlockedCountries(context.Context, *ListLoginRiskBlockedCountriesRequest) (*ListLoginRiskBlockedCountriesResponse, error)
|
||||||
mustEmbedUnimplementedCountryQueryServiceServer()
|
mustEmbedUnimplementedCountryQueryServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1543,6 +1556,9 @@ type UnimplementedCountryQueryServiceServer struct{}
|
|||||||
func (UnimplementedCountryQueryServiceServer) ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error) {
|
func (UnimplementedCountryQueryServiceServer) ListRegistrationCountries(context.Context, *ListRegistrationCountriesRequest) (*ListRegistrationCountriesResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationCountries not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListRegistrationCountries not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedCountryQueryServiceServer) ListLoginRiskBlockedCountries(context.Context, *ListLoginRiskBlockedCountriesRequest) (*ListLoginRiskBlockedCountriesResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method ListLoginRiskBlockedCountries not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedCountryQueryServiceServer) mustEmbedUnimplementedCountryQueryServiceServer() {}
|
func (UnimplementedCountryQueryServiceServer) mustEmbedUnimplementedCountryQueryServiceServer() {}
|
||||||
func (UnimplementedCountryQueryServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedCountryQueryServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@ -1582,6 +1598,24 @@ func _CountryQueryService_ListRegistrationCountries_Handler(srv interface{}, ctx
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _CountryQueryService_ListLoginRiskBlockedCountries_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ListLoginRiskBlockedCountriesRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(CountryQueryServiceServer).ListLoginRiskBlockedCountries(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: CountryQueryService_ListLoginRiskBlockedCountries_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(CountryQueryServiceServer).ListLoginRiskBlockedCountries(ctx, req.(*ListLoginRiskBlockedCountriesRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// CountryQueryService_ServiceDesc is the grpc.ServiceDesc for CountryQueryService service.
|
// CountryQueryService_ServiceDesc is the grpc.ServiceDesc for CountryQueryService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@ -1593,6 +1627,10 @@ var CountryQueryService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "ListRegistrationCountries",
|
MethodName: "ListRegistrationCountries",
|
||||||
Handler: _CountryQueryService_ListRegistrationCountries_Handler,
|
Handler: _CountryQueryService_ListRegistrationCountries_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "ListLoginRiskBlockedCountries",
|
||||||
|
Handler: _CountryQueryService_ListLoginRiskBlockedCountries_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "proto/user/v1/user.proto",
|
Metadata: "proto/user/v1/user.proto",
|
||||||
|
|||||||
286
docs/flutter对接/表情包Flutter对接.md
Normal file
286
docs/flutter对接/表情包Flutter对接.md
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# 表情包 Flutter 对接
|
||||||
|
|
||||||
|
本文描述 Flutter App 拉取表情包列表的 HTTP 接口。表情包由后台管理维护,App 只读取 gateway 返回的已启用资源;免费/付费字段用于展示和后续业务分流,不在本接口内完成购买或授权判断。
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
Base URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://<gateway-host>
|
||||||
|
```
|
||||||
|
|
||||||
|
本地开发:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:13000
|
||||||
|
```
|
||||||
|
|
||||||
|
| 功能 | Method | Path | 鉴权 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 表情包列表 | `GET` | `/api/v1/emoji-packs` | 不需要登录 |
|
||||||
|
|
||||||
|
请求头:
|
||||||
|
|
||||||
|
| Header | 必填 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `X-App-Code` | 否 | App 编码,默认 `lalu`。 |
|
||||||
|
| `X-App-Package` | 否 | 包名解析 App 时使用。 |
|
||||||
|
| `X-App-Platform` | 否 | `android` / `ios`。 |
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/emoji-packs?page=1&page_size=20®ion_id=1001
|
||||||
|
X-App-Code: lalu
|
||||||
|
X-App-Platform: ios
|
||||||
|
```
|
||||||
|
|
||||||
|
## 请求参数
|
||||||
|
|
||||||
|
Query:
|
||||||
|
|
||||||
|
| 字段 | 必填 | 默认 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `page` | 否 | `1` | 页码,从 1 开始;必须是正整数。 |
|
||||||
|
| `page_size` | 否 | `20` | 每页数量;必须是正整数。Flutter 端统一使用该字段。 |
|
||||||
|
| `keyword` | 否 | 空 | 按表情包名称或资源编码检索。 |
|
||||||
|
| `region_id` | 否 | 不过滤 | 用户当前区域 ID;传入后返回全局表情包和该区域可用表情包。缺省或传 `0` 返回全部已启用表情包。 |
|
||||||
|
|
||||||
|
## 返回值
|
||||||
|
|
||||||
|
成功响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "OK",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": "req_abc",
|
||||||
|
"data": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"resource_id": 162,
|
||||||
|
"resource_code": "emoji_pack_1778828784753000000",
|
||||||
|
"name": "Rocket",
|
||||||
|
"category": "热门",
|
||||||
|
"pricing_type": "paid",
|
||||||
|
"cover_url": "https://cdn.example/emoji/rocket.png",
|
||||||
|
"animation_url": "https://cdn.example/emoji/rocket.svga",
|
||||||
|
"region_ids": [1001],
|
||||||
|
"is_global": false,
|
||||||
|
"sort_order": 10,
|
||||||
|
"updated_at_ms": 1778828784753
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段规则:
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `items` | 当前页表情包列表,只包含后台启用状态的表情包。 |
|
||||||
|
| `total` | 当前 query 条件下的总数,用于分页。 |
|
||||||
|
| `resource_id` | 表情包资源 ID,后续发送、展示或授权判断应使用该 ID。 |
|
||||||
|
| `resource_code` | 资源编码,主要用于日志和排查,不建议作为业务主键。 |
|
||||||
|
| `name` | 表情包名称。 |
|
||||||
|
| `category` | 表情包分类,用于列表分组或筛选;历史数据缺省时返回 `默认`。 |
|
||||||
|
| `pricing_type` | `free` / `paid`;客户端展示为免费/付费标签。 |
|
||||||
|
| `cover_url` | 静态封面图,优先用于列表和面板入口。 |
|
||||||
|
| `animation_url` | 动态资源地址,文件类型由后台上传内容决定,可能是 `webp`、`svga`、`pag` 或其他客户端已支持格式。 |
|
||||||
|
| `region_ids` | 可用区域 ID;空数组表示全局可用。 |
|
||||||
|
| `is_global` | `true` 表示全局可用。 |
|
||||||
|
| `sort_order` | 后台排序值,列表已经按服务端资源顺序返回,客户端不要重新反向排序。 |
|
||||||
|
| `updated_at_ms` | Unix epoch milliseconds,可作为本地缓存刷新依据。 |
|
||||||
|
|
||||||
|
## Flutter 解析示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ApiEnvelope<T> {
|
||||||
|
ApiEnvelope({
|
||||||
|
required this.code,
|
||||||
|
required this.message,
|
||||||
|
required this.requestId,
|
||||||
|
this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final String requestId;
|
||||||
|
final T? data;
|
||||||
|
|
||||||
|
factory ApiEnvelope.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
T Function(Object? value) parseData,
|
||||||
|
) {
|
||||||
|
return ApiEnvelope<T>(
|
||||||
|
code: json['code'] as String? ?? '',
|
||||||
|
message: json['message'] as String? ?? '',
|
||||||
|
requestId: json['request_id'] as String? ?? '',
|
||||||
|
data: json.containsKey('data') ? parseData(json['data']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmojiPackPage {
|
||||||
|
EmojiPackPage({
|
||||||
|
required this.items,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<EmojiPackItem> items;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int pageSize;
|
||||||
|
|
||||||
|
factory EmojiPackPage.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EmojiPackPage(
|
||||||
|
items: (json['items'] as List<dynamic>? ?? const [])
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(EmojiPackItem.fromJson)
|
||||||
|
.toList(),
|
||||||
|
total: (json['total'] as num?)?.toInt() ?? 0,
|
||||||
|
page: (json['page'] as num?)?.toInt() ?? 1,
|
||||||
|
pageSize: (json['page_size'] as num?)?.toInt() ?? 20,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmojiPackItem {
|
||||||
|
EmojiPackItem({
|
||||||
|
required this.resourceId,
|
||||||
|
required this.resourceCode,
|
||||||
|
required this.name,
|
||||||
|
required this.category,
|
||||||
|
required this.pricingType,
|
||||||
|
required this.coverUrl,
|
||||||
|
required this.animationUrl,
|
||||||
|
required this.regionIds,
|
||||||
|
required this.isGlobal,
|
||||||
|
required this.sortOrder,
|
||||||
|
required this.updatedAtMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int resourceId;
|
||||||
|
final String resourceCode;
|
||||||
|
final String name;
|
||||||
|
final String category;
|
||||||
|
final String pricingType;
|
||||||
|
final String coverUrl;
|
||||||
|
final String animationUrl;
|
||||||
|
final List<int> regionIds;
|
||||||
|
final bool isGlobal;
|
||||||
|
final int sortOrder;
|
||||||
|
final int updatedAtMs;
|
||||||
|
|
||||||
|
bool get isPaid => pricingType == 'paid';
|
||||||
|
String get pricingLabel => isPaid ? '付费' : '免费';
|
||||||
|
|
||||||
|
factory EmojiPackItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
return EmojiPackItem(
|
||||||
|
resourceId: (json['resource_id'] as num?)?.toInt() ?? 0,
|
||||||
|
resourceCode: json['resource_code'] as String? ?? '',
|
||||||
|
name: json['name'] as String? ?? '',
|
||||||
|
category: json['category'] as String? ?? '默认',
|
||||||
|
pricingType: json['pricing_type'] as String? ?? 'free',
|
||||||
|
coverUrl: json['cover_url'] as String? ?? '',
|
||||||
|
animationUrl: json['animation_url'] as String? ?? '',
|
||||||
|
regionIds: (json['region_ids'] as List<dynamic>? ?? const [])
|
||||||
|
.whereType<num>()
|
||||||
|
.map((value) => value.toInt())
|
||||||
|
.toList(),
|
||||||
|
isGlobal: json['is_global'] as bool? ?? false,
|
||||||
|
sortOrder: (json['sort_order'] as num?)?.toInt() ?? 0,
|
||||||
|
updatedAtMs: (json['updated_at_ms'] as num?)?.toInt() ?? 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
调用示例:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ApiException implements Exception {
|
||||||
|
ApiException({
|
||||||
|
required this.code,
|
||||||
|
required this.message,
|
||||||
|
required this.requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
final String message;
|
||||||
|
final String requestId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'ApiException($code, requestId: $requestId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<EmojiPackPage> fetchEmojiPacks({
|
||||||
|
required Dio dio,
|
||||||
|
String appCode = 'lalu',
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 20,
|
||||||
|
int? regionId,
|
||||||
|
String keyword = '',
|
||||||
|
}) async {
|
||||||
|
final response = await dio.get<Map<String, dynamic>>(
|
||||||
|
'/api/v1/emoji-packs',
|
||||||
|
queryParameters: <String, dynamic>{
|
||||||
|
'page': page,
|
||||||
|
'page_size': pageSize,
|
||||||
|
if (regionId != null) 'region_id': regionId,
|
||||||
|
if (keyword.trim().isNotEmpty) 'keyword': keyword.trim(),
|
||||||
|
},
|
||||||
|
options: Options(headers: <String, String>{'X-App-Code': appCode}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final envelope = ApiEnvelope<EmojiPackPage>.fromJson(
|
||||||
|
response.data ?? const <String, dynamic>{},
|
||||||
|
(value) => EmojiPackPage.fromJson(value as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
if (envelope.code != 'OK' || envelope.data == null) {
|
||||||
|
throw ApiException(
|
||||||
|
code: envelope.code,
|
||||||
|
message: envelope.message,
|
||||||
|
requestId: envelope.requestId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return envelope.data!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
所有响应先解析外层 envelope。只有 HTTP status 为 `200` 且 `code == "OK"` 时读取 `data`;其他情况不要使用 `data`。
|
||||||
|
|
||||||
|
| HTTP status | `code` | 处理方式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `400` | `INVALID_ARGUMENT` | 请求参数非法,例如 `page`、`page_size`、`region_id` 不是合法数字;客户端修正参数后再请求,不做自动重试。 |
|
||||||
|
| `502` | `UPSTREAM_ERROR` | 后端依赖服务不可用;展示上次缓存或空态,允许用户手动重试,可做指数退避。 |
|
||||||
|
| `500` | `INTERNAL_ERROR` | 服务端未知错误;展示通用错误,把 `request_id` 写入客户端日志用于排查。 |
|
||||||
|
| 网络错误 | 无 envelope | 超时、断网或 TLS 失败;优先展示缓存,恢复网络后重新拉取当前页。 |
|
||||||
|
|
||||||
|
失败响应示例:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "INVALID_ARGUMENT",
|
||||||
|
"message": "invalid argument",
|
||||||
|
"request_id": "req_bad"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
客户端缓存建议:
|
||||||
|
|
||||||
|
| 场景 | 建议 |
|
||||||
|
| --- | --- |
|
||||||
|
| 首次进入表情面板 | 请求 `page=1&page_size=20`,有用户区域时带 `region_id`。 |
|
||||||
|
| 本地缓存 key | 至少包含 `app_code`、`region_id`、`keyword`、`page`、`page_size`。 |
|
||||||
|
| 刷新判断 | 同一个 `resource_id` 的 `updated_at_ms` 变大时刷新本地资源和封面缓存。 |
|
||||||
|
| 付费展示 | `pricing_type == "paid"` 只表示付费标签;能否发送该表情包仍以发送表情或用户资源授权接口的服务端结果为准。 |
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -10,14 +11,18 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
COPY server/admin/go.mod server/admin/go.sum ./server/admin/
|
COPY server/admin/go.mod server/admin/go.sum ./server/admin/
|
||||||
WORKDIR /src/server/admin
|
WORKDIR /src/server/admin
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY api ./api
|
COPY api ./api
|
||||||
COPY server/admin ./server/admin
|
COPY server/admin ./server/admin
|
||||||
|
|
||||||
WORKDIR /src/server/admin
|
WORKDIR /src/server/admin
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./cmd/server
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/config"
|
"hyapp-admin-server/internal/config"
|
||||||
"hyapp-admin-server/internal/integration/activityclient"
|
"hyapp-admin-server/internal/integration/activityclient"
|
||||||
"hyapp-admin-server/internal/integration/gameclient"
|
"hyapp-admin-server/internal/integration/gameclient"
|
||||||
|
"hyapp-admin-server/internal/integration/roomclient"
|
||||||
"hyapp-admin-server/internal/integration/userclient"
|
"hyapp-admin-server/internal/integration/userclient"
|
||||||
"hyapp-admin-server/internal/integration/walletclient"
|
"hyapp-admin-server/internal/integration/walletclient"
|
||||||
jobrunner "hyapp-admin-server/internal/job"
|
jobrunner "hyapp-admin-server/internal/job"
|
||||||
@ -41,6 +42,7 @@ import (
|
|||||||
menumodule "hyapp-admin-server/internal/modules/menu"
|
menumodule "hyapp-admin-server/internal/modules/menu"
|
||||||
paymentmodule "hyapp-admin-server/internal/modules/payment"
|
paymentmodule "hyapp-admin-server/internal/modules/payment"
|
||||||
rbacmodule "hyapp-admin-server/internal/modules/rbac"
|
rbacmodule "hyapp-admin-server/internal/modules/rbac"
|
||||||
|
regionblockmodule "hyapp-admin-server/internal/modules/regionblock"
|
||||||
registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward"
|
registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward"
|
||||||
resourcemodule "hyapp-admin-server/internal/modules/resource"
|
resourcemodule "hyapp-admin-server/internal/modules/resource"
|
||||||
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
|
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
|
||||||
@ -160,6 +162,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer walletConn.Close()
|
defer walletConn.Close()
|
||||||
|
|
||||||
|
roomConn, err := grpc.Dial(cfg.RoomService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
if err != nil {
|
||||||
|
fatalRuntime("connect_room_service_failed", err)
|
||||||
|
}
|
||||||
|
defer roomConn.Close()
|
||||||
|
|
||||||
activityConn, err := grpc.Dial(cfg.ActivityService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
activityConn, err := grpc.Dial(cfg.ActivityService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalRuntime("connect_activity_service_failed", err)
|
fatalRuntime("connect_activity_service_failed", err)
|
||||||
@ -218,8 +226,9 @@ func main() {
|
|||||||
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
|
||||||
RBAC: rbacmodule.New(store, auditHandler),
|
RBAC: rbacmodule.New(store, auditHandler),
|
||||||
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||||
|
RegionBlock: regionblockmodule.New(userDB, auditHandler),
|
||||||
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),
|
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),
|
||||||
RoomAdmin: roomadminmodule.New(roomDB, userDB, auditHandler),
|
RoomAdmin: roomadminmodule.New(roomDB, userDB, roomclient.NewGRPC(roomConn), auditHandler),
|
||||||
Search: searchmodule.New(store),
|
Search: searchmodule.New(store),
|
||||||
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
|
Upload: uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,9 @@ user_service:
|
|||||||
wallet_service:
|
wallet_service:
|
||||||
addr: "10.2.1.15:13004"
|
addr: "10.2.1.15:13004"
|
||||||
request_timeout: "3s"
|
request_timeout: "3s"
|
||||||
|
room_service:
|
||||||
|
addr: "10.2.1.15:13001"
|
||||||
|
request_timeout: "3s"
|
||||||
activity_service:
|
activity_service:
|
||||||
addr: "10.2.1.16:13006"
|
addr: "10.2.1.16:13006"
|
||||||
request_timeout: "3s"
|
request_timeout: "3s"
|
||||||
|
|||||||
@ -40,6 +40,9 @@ user_service:
|
|||||||
wallet_service:
|
wallet_service:
|
||||||
addr: "127.0.0.1:13004"
|
addr: "127.0.0.1:13004"
|
||||||
request_timeout: "3s"
|
request_timeout: "3s"
|
||||||
|
room_service:
|
||||||
|
addr: "127.0.0.1:13001"
|
||||||
|
request_timeout: "3s"
|
||||||
activity_service:
|
activity_service:
|
||||||
addr: "127.0.0.1:13006"
|
addr: "127.0.0.1:13006"
|
||||||
request_timeout: "3s"
|
request_timeout: "3s"
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type Config struct {
|
|||||||
Redis RedisConfig `yaml:"redis"`
|
Redis RedisConfig `yaml:"redis"`
|
||||||
Jobs JobsConfig `yaml:"jobs"`
|
Jobs JobsConfig `yaml:"jobs"`
|
||||||
WalletService WalletServiceConfig `yaml:"wallet_service"`
|
WalletService WalletServiceConfig `yaml:"wallet_service"`
|
||||||
|
RoomService RoomServiceConfig `yaml:"room_service"`
|
||||||
ActivityService ActivityServiceConfig `yaml:"activity_service"`
|
ActivityService ActivityServiceConfig `yaml:"activity_service"`
|
||||||
GameService GameServiceConfig `yaml:"game_service"`
|
GameService GameServiceConfig `yaml:"game_service"`
|
||||||
}
|
}
|
||||||
@ -56,6 +57,11 @@ type WalletServiceConfig struct {
|
|||||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RoomServiceConfig struct {
|
||||||
|
Addr string `yaml:"addr"`
|
||||||
|
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
type ActivityServiceConfig struct {
|
type ActivityServiceConfig struct {
|
||||||
Addr string `yaml:"addr"`
|
Addr string `yaml:"addr"`
|
||||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||||
@ -143,6 +149,10 @@ func Default() Config {
|
|||||||
Addr: "127.0.0.1:13004",
|
Addr: "127.0.0.1:13004",
|
||||||
RequestTimeout: 3 * time.Second,
|
RequestTimeout: 3 * time.Second,
|
||||||
},
|
},
|
||||||
|
RoomService: RoomServiceConfig{
|
||||||
|
Addr: "127.0.0.1:13001",
|
||||||
|
RequestTimeout: 3 * time.Second,
|
||||||
|
},
|
||||||
ActivityService: ActivityServiceConfig{
|
ActivityService: ActivityServiceConfig{
|
||||||
Addr: "127.0.0.1:13006",
|
Addr: "127.0.0.1:13006",
|
||||||
RequestTimeout: 3 * time.Second,
|
RequestTimeout: 3 * time.Second,
|
||||||
@ -241,6 +251,13 @@ func (cfg *Config) Normalize() {
|
|||||||
if cfg.WalletService.RequestTimeout <= 0 {
|
if cfg.WalletService.RequestTimeout <= 0 {
|
||||||
cfg.WalletService.RequestTimeout = 3 * time.Second
|
cfg.WalletService.RequestTimeout = 3 * time.Second
|
||||||
}
|
}
|
||||||
|
cfg.RoomService.Addr = strings.TrimSpace(cfg.RoomService.Addr)
|
||||||
|
if cfg.RoomService.Addr == "" {
|
||||||
|
cfg.RoomService.Addr = "127.0.0.1:13001"
|
||||||
|
}
|
||||||
|
if cfg.RoomService.RequestTimeout <= 0 {
|
||||||
|
cfg.RoomService.RequestTimeout = 3 * time.Second
|
||||||
|
}
|
||||||
cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
|
cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
|
||||||
if cfg.ActivityService.RequestTimeout <= 0 {
|
if cfg.ActivityService.RequestTimeout <= 0 {
|
||||||
cfg.ActivityService.RequestTimeout = 3 * time.Second
|
cfg.ActivityService.RequestTimeout = 3 * time.Second
|
||||||
@ -332,6 +349,12 @@ func (cfg Config) Validate() error {
|
|||||||
if cfg.WalletService.RequestTimeout <= 0 {
|
if cfg.WalletService.RequestTimeout <= 0 {
|
||||||
return errors.New("wallet_service.request_timeout must be greater than 0")
|
return errors.New("wallet_service.request_timeout must be greater than 0")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(cfg.RoomService.Addr) == "" {
|
||||||
|
return errors.New("room_service.addr is required")
|
||||||
|
}
|
||||||
|
if cfg.RoomService.RequestTimeout <= 0 {
|
||||||
|
return errors.New("room_service.request_timeout must be greater than 0")
|
||||||
|
}
|
||||||
if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
|
if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
|
||||||
return errors.New("activity_service.addr is required")
|
return errors.New("activity_service.addr is required")
|
||||||
}
|
}
|
||||||
|
|||||||
114
server/admin/internal/integration/roomclient/client.go
Normal file
114
server/admin/internal/integration/roomclient/client.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package roomclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error)
|
||||||
|
ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseRoomRequest struct {
|
||||||
|
RequestID string
|
||||||
|
RoomID string
|
||||||
|
ActorUserID int64
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloseRoomResult struct {
|
||||||
|
Applied bool
|
||||||
|
RoomVersion int64
|
||||||
|
ServerTimeMs int64
|
||||||
|
RoomStatus string
|
||||||
|
OnlineUserNum int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReopenRoomRequest struct {
|
||||||
|
RequestID string
|
||||||
|
RoomID string
|
||||||
|
ActorUserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type GRPCClient struct {
|
||||||
|
client roomv1.RoomCommandServiceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||||
|
return &GRPCClient{client: roomv1.NewRoomCommandServiceClient(conn)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error) {
|
||||||
|
return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, firstNonEmpty(req.Reason, "admin_closed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error) {
|
||||||
|
return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, "admin_reopen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) setRoomLifecycle(ctx context.Context, rawRequestID string, rawRoomID string, actorUserID int64, rawReason string) (*CloseRoomResult, error) {
|
||||||
|
if c == nil || c.client == nil {
|
||||||
|
return nil, fmt.Errorf("room service client is not configured")
|
||||||
|
}
|
||||||
|
roomID := strings.TrimSpace(rawRoomID)
|
||||||
|
reason := strings.TrimSpace(rawReason)
|
||||||
|
if roomID == "" || actorUserID <= 0 || reason == "" {
|
||||||
|
return nil, fmt.Errorf("room lifecycle request is incomplete")
|
||||||
|
}
|
||||||
|
requestID := strings.TrimSpace(rawRequestID)
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = fmt.Sprintf("admin-room-lifecycle-%s-%d", roomID, time.Now().UnixMilli())
|
||||||
|
}
|
||||||
|
resp, err := c.client.CloseRoom(ctx, &roomv1.CloseRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{
|
||||||
|
RequestId: requestID,
|
||||||
|
CommandId: commandID(requestID, reason, roomID),
|
||||||
|
ActorUserId: actorUserID,
|
||||||
|
RoomId: roomID,
|
||||||
|
AppCode: appctx.FromContext(ctx),
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
Reason: reason,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := resp.GetResult()
|
||||||
|
room := resp.GetRoom()
|
||||||
|
return &CloseRoomResult{
|
||||||
|
Applied: result.GetApplied(),
|
||||||
|
RoomVersion: result.GetRoomVersion(),
|
||||||
|
ServerTimeMs: result.GetServerTimeMs(),
|
||||||
|
RoomStatus: room.GetStatus(),
|
||||||
|
OnlineUserNum: len(room.GetOnlineUsers()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandID(requestID string, reason string, roomID string) string {
|
||||||
|
base := strings.TrimSpace(requestID)
|
||||||
|
if base == "" {
|
||||||
|
base = fmt.Sprintf("admin-room-lifecycle-%d", time.Now().UnixMilli())
|
||||||
|
}
|
||||||
|
value := base + ":" + strings.TrimSpace(reason) + ":" + strings.TrimSpace(roomID)
|
||||||
|
if len(value) <= 128 {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return value[:128]
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.TrimSpace(value) != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
56
server/admin/internal/modules/regionblock/handler.go
Normal file
56
server/admin/internal/modules/regionblock/handler.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package regionblock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
audit shared.OperationLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
type replaceRequest struct {
|
||||||
|
Keywords *[]string `json:"keywords"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
||||||
|
return &Handler{service: NewService(userDB), audit: audit}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListRegionBlocks 返回用户管理 > 地区屏蔽页面需要的当前屏蔽词列表。
|
||||||
|
func (h *Handler) ListRegionBlocks(c *gin.Context) {
|
||||||
|
items, err := h.service.List(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取地区屏蔽配置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceRegionBlocks 保存整页地区屏蔽词配置;空数组表示清空屏蔽词。
|
||||||
|
func (h *Handler) ReplaceRegionBlocks(c *gin.Context) {
|
||||||
|
var req replaceRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil || req.Keywords == nil {
|
||||||
|
response.BadRequest(c, "地区屏蔽词参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items, err := h.service.Replace(c.Request.Context(), int64(shared.ActorFromContext(c).UserID), *req.Keywords)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errInvalidPayload) {
|
||||||
|
response.BadRequest(c, "地区屏蔽词参数不正确")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.ServerError(c, "保存地区屏蔽配置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared.OperationLogWithResourceID(c, h.audit, "replace-region-blocks", "login_risk_country_blocks", middleware.CurrentRequestID(c), "success", fmt.Sprintf("keywords=%d", len(items)))
|
||||||
|
response.OK(c, gin.H{"items": items, "total": len(items)})
|
||||||
|
}
|
||||||
16
server/admin/internal/modules/regionblock/routes.go
Normal file
16
server/admin/internal/modules/regionblock/routes.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package regionblock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
protected.GET("/admin/users/region-blocks", middleware.RequirePermission("region-block:view"), h.ListRegionBlocks)
|
||||||
|
protected.PUT("/admin/users/region-blocks", middleware.RequirePermission("region-block:update"), h.ReplaceRegionBlocks)
|
||||||
|
}
|
||||||
194
server/admin/internal/modules/regionblock/service.go
Normal file
194
server/admin/internal/modules/regionblock/service.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package regionblock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxRegionBlockKeywords = 200
|
||||||
|
maxRegionBlockKeywordLength = 128
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
countryCodePattern = regexp.MustCompile(`^[A-Za-z]{2,3}$`)
|
||||||
|
errInvalidPayload = errors.New("region block payload invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
userDB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegionBlock struct {
|
||||||
|
BlockID int64 `json:"block_id"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAtMS int64 `json:"created_at_ms"`
|
||||||
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type normalizedKeyword struct {
|
||||||
|
Keyword string
|
||||||
|
CountryCode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type countryCodeQuerier interface {
|
||||||
|
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(userDB *sql.DB) *Service {
|
||||||
|
return &Service{userDB: userDB}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 返回当前 App 已启用的登录地区屏蔽词。
|
||||||
|
// 表属于 user-service 登录风控事实,admin-server 只作为后台维护入口直接写 user DB。
|
||||||
|
func (s *Service) List(ctx context.Context) ([]RegionBlock, error) {
|
||||||
|
if s.userDB == nil {
|
||||||
|
return nil, errors.New("user db is not configured")
|
||||||
|
}
|
||||||
|
return queryEnabledBlocks(ctx, s.userDB, appctx.FromContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace 用页面提交的词表整体替换当前 App 的生效配置。
|
||||||
|
// 整体替换比逐条增删更适合配置页保存语义,也能避免客户端和服务端出现半旧半新的屏蔽词集合。
|
||||||
|
func (s *Service) Replace(ctx context.Context, actorID int64, keywords []string) ([]RegionBlock, error) {
|
||||||
|
if s.userDB == nil {
|
||||||
|
return nil, errors.New("user db is not configured")
|
||||||
|
}
|
||||||
|
normalized, err := normalizeKeywords(ctx, s.userDB, appctx.FromContext(ctx), keywords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tx, err := s.userDB.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
nowMS := time.Now().UTC().UnixMilli()
|
||||||
|
appCode := appctx.FromContext(ctx)
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE login_risk_country_blocks
|
||||||
|
SET enabled = 0, updated_by_user_id = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND enabled = 1
|
||||||
|
`, nullableActor(actorID), nowMS, appCode); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, keyword := range normalized {
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
INSERT INTO login_risk_country_blocks (
|
||||||
|
app_code, keyword, country_code, enabled,
|
||||||
|
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, 1, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
country_code = VALUES(country_code),
|
||||||
|
enabled = 1,
|
||||||
|
updated_by_user_id = VALUES(updated_by_user_id),
|
||||||
|
updated_at_ms = VALUES(updated_at_ms)
|
||||||
|
`, appCode, keyword.Keyword, keyword.CountryCode, nullableActor(actorID), nullableActor(actorID), nowMS, nowMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.List(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKeywords(ctx context.Context, querier countryCodeQuerier, appCode string, raw []string) ([]normalizedKeyword, error) {
|
||||||
|
if len(raw) > maxRegionBlockKeywords {
|
||||||
|
return nil, errInvalidPayload
|
||||||
|
}
|
||||||
|
result := make([]normalizedKeyword, 0, len(raw))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, item := range raw {
|
||||||
|
keyword := strings.TrimSpace(item)
|
||||||
|
if keyword == "" || utf8.RuneCountInString(keyword) > maxRegionBlockKeywordLength {
|
||||||
|
return nil, errInvalidPayload
|
||||||
|
}
|
||||||
|
if countryCodePattern.MatchString(keyword) {
|
||||||
|
keyword = strings.ToUpper(keyword)
|
||||||
|
}
|
||||||
|
key := strings.ToLower(keyword)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
countryCode, err := resolveCountryCode(ctx, querier, appCode, keyword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
result = append(result, normalizedKeyword{Keyword: keyword, CountryCode: countryCode})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCountryCode(ctx context.Context, querier countryCodeQuerier, appCode string, keyword string) (string, error) {
|
||||||
|
if countryCodePattern.MatchString(keyword) {
|
||||||
|
return strings.ToUpper(keyword), nil
|
||||||
|
}
|
||||||
|
upperKeyword := strings.ToUpper(keyword)
|
||||||
|
var countryCode string
|
||||||
|
err := querier.QueryRowContext(ctx, `
|
||||||
|
SELECT country_code
|
||||||
|
FROM countries
|
||||||
|
WHERE app_code = ?
|
||||||
|
AND (
|
||||||
|
country_name = ?
|
||||||
|
OR country_display_name = ?
|
||||||
|
OR UPPER(country_code) = ?
|
||||||
|
OR UPPER(COALESCE(iso_alpha3, '')) = ?
|
||||||
|
)
|
||||||
|
ORDER BY enabled DESC, sort_order ASC, country_id ASC
|
||||||
|
LIMIT 1
|
||||||
|
`, appCode, keyword, keyword, upperKeyword, upperKeyword).Scan(&countryCode)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.ToUpper(strings.TrimSpace(countryCode)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryEnabledBlocks(ctx context.Context, db *sql.DB, appCode string) ([]RegionBlock, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
|
||||||
|
FROM login_risk_country_blocks
|
||||||
|
WHERE app_code = ? AND enabled = 1
|
||||||
|
ORDER BY country_code ASC, keyword ASC, block_id ASC
|
||||||
|
`, appCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]RegionBlock, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item RegionBlock
|
||||||
|
if err := rows.Scan(&item.BlockID, &item.Keyword, &item.CountryCode, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableActor(actorID int64) any {
|
||||||
|
if actorID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return actorID
|
||||||
|
}
|
||||||
@ -1,10 +1,6 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import (
|
import walletv1 "hyapp.local/api/proto/wallet/v1"
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resourceDTO struct {
|
type resourceDTO struct {
|
||||||
AppCode string `json:"appCode"`
|
AppCode string `json:"appCode"`
|
||||||
@ -90,6 +86,8 @@ type emojiPackDTO struct {
|
|||||||
ResourceCode string `json:"resourceCode"`
|
ResourceCode string `json:"resourceCode"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricingType"`
|
||||||
CoverURL string `json:"coverUrl"`
|
CoverURL string `json:"coverUrl"`
|
||||||
AnimationURL string `json:"animationUrl"`
|
AnimationURL string `json:"animationUrl"`
|
||||||
RegionIDs []int64 `json:"regionIds"`
|
RegionIDs []int64 `json:"regionIds"`
|
||||||
@ -116,6 +114,7 @@ type grantDTO struct {
|
|||||||
GrantID string `json:"grantId"`
|
GrantID string `json:"grantId"`
|
||||||
CommandID string `json:"commandId"`
|
CommandID string `json:"commandId"`
|
||||||
TargetUserID int64 `json:"targetUserId,string"`
|
TargetUserID int64 `json:"targetUserId,string"`
|
||||||
|
TargetUser *grantUserDTO `json:"targetUser,omitempty"`
|
||||||
GrantSource string `json:"grantSource"`
|
GrantSource string `json:"grantSource"`
|
||||||
GrantSubjectType string `json:"grantSubjectType"`
|
GrantSubjectType string `json:"grantSubjectType"`
|
||||||
GrantSubjectID string `json:"grantSubjectId"`
|
GrantSubjectID string `json:"grantSubjectId"`
|
||||||
@ -128,6 +127,13 @@ type grantDTO struct {
|
|||||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type grantUserDTO struct {
|
||||||
|
UserID int64 `json:"userId,string"`
|
||||||
|
DisplayUserID string `json:"displayUserId,omitempty"`
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type operatorDTO struct {
|
type operatorDTO struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
UserID int64 `json:"userId,string,omitempty"`
|
UserID int64 `json:"userId,string,omitempty"`
|
||||||
@ -244,15 +250,18 @@ func emojiPackFromProto(item *walletv1.Resource) emojiPackDTO {
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return emojiPackDTO{}
|
return emojiPackDTO{}
|
||||||
}
|
}
|
||||||
|
metadata := emojiPackMetadataFromJSON(item.GetMetadataJson())
|
||||||
return emojiPackDTO{
|
return emojiPackDTO{
|
||||||
AppCode: item.GetAppCode(),
|
AppCode: item.GetAppCode(),
|
||||||
ResourceID: item.GetResourceId(),
|
ResourceID: item.GetResourceId(),
|
||||||
ResourceCode: item.GetResourceCode(),
|
ResourceCode: item.GetResourceCode(),
|
||||||
Name: item.GetName(),
|
Name: item.GetName(),
|
||||||
Status: item.GetStatus(),
|
Status: item.GetStatus(),
|
||||||
|
Category: metadata.Category,
|
||||||
|
PricingType: metadata.PricingType,
|
||||||
CoverURL: firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()),
|
CoverURL: firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()),
|
||||||
AnimationURL: item.GetAnimationUrl(),
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
RegionIDs: regionIDsFromMetadata(item.GetMetadataJson()),
|
RegionIDs: metadata.RegionIDs,
|
||||||
SortOrder: item.GetSortOrder(),
|
SortOrder: item.GetSortOrder(),
|
||||||
CreatedAtMS: item.GetCreatedAtMs(),
|
CreatedAtMS: item.GetCreatedAtMs(),
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
@ -260,13 +269,7 @@ func emojiPackFromProto(item *walletv1.Resource) emojiPackDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func regionIDsFromMetadata(metadataJSON string) []int64 {
|
func regionIDsFromMetadata(metadataJSON string) []int64 {
|
||||||
var payload struct {
|
return emojiPackMetadataFromJSON(metadataJSON).RegionIDs
|
||||||
RegionIDs []int64 `json:"region_ids"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return normalizeRegionIDs(payload.RegionIDs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstNonEmpty(values ...string) string {
|
func firstNonEmpty(values ...string) string {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package resource
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -105,6 +106,39 @@ func (h *Handler) ListEmojiPacks(c *gin.Context) {
|
|||||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListEmojiPackCategories(c *gin.Context) {
|
||||||
|
const pageSize int32 = 100
|
||||||
|
categories := map[string]struct{}{}
|
||||||
|
for page := int32(1); ; page++ {
|
||||||
|
resp, err := h.wallet.ListResources(c.Request.Context(), &walletv1.ListResourcesRequest{
|
||||||
|
RequestId: middleware.CurrentRequestID(c),
|
||||||
|
AppCode: appctx.FromContext(c.Request.Context()),
|
||||||
|
ResourceType: resourceTypeEmojiPack,
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.ServerError(c, "获取表情包分类失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, item := range resp.GetResources() {
|
||||||
|
category := emojiPackMetadataFromJSON(item.GetMetadataJson()).Category
|
||||||
|
if category != "" {
|
||||||
|
categories[category] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(resp.GetResources()) == 0 || int64(page)*int64(pageSize) >= resp.GetTotal() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items := make([]string, 0, len(categories))
|
||||||
|
for category := range categories {
|
||||||
|
items = append(items, category)
|
||||||
|
}
|
||||||
|
sort.Strings(items)
|
||||||
|
response.OK(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) CreateEmojiPack(c *gin.Context) {
|
func (h *Handler) CreateEmojiPack(c *gin.Context) {
|
||||||
var req emojiPackRequest
|
var req emojiPackRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@ -385,6 +419,10 @@ func (h *Handler) ListResourceGrants(c *gin.Context) {
|
|||||||
response.ServerError(c, "获取资源赠送操作人失败")
|
response.ServerError(c, "获取资源赠送操作人失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := h.enrichGrantTargets(c.Request.Context(), items); err != nil {
|
||||||
|
response.ServerError(c, "获取资源赠送用户信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,66 @@ type managerOperator struct {
|
|||||||
Avatar string
|
Avatar string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) enrichGrantTargets(ctx context.Context, grants []grantDTO) error {
|
||||||
|
ids := collectGrantTargetIDs(grants)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
users := make(map[int64]grantUserDTO, len(ids))
|
||||||
|
if h.userDB != nil {
|
||||||
|
var err error
|
||||||
|
users, err = h.queryGrantTargetUsers(ctx, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyGrantTargetUsers(grants, users)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) queryGrantTargetUsers(ctx context.Context, ids []int64) (map[int64]grantUserDTO, error) {
|
||||||
|
out := make(map[int64]grantUserDTO, len(ids))
|
||||||
|
if len(ids) == 0 || h.userDB == nil {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
args := append([]any{appctx.FromContext(ctx)}, int64Args(ids)...)
|
||||||
|
rows, err := h.userDB.QueryContext(ctx, `
|
||||||
|
SELECT user_id,
|
||||||
|
current_display_user_id,
|
||||||
|
COALESCE(username, ''),
|
||||||
|
COALESCE(avatar, '')
|
||||||
|
FROM users
|
||||||
|
WHERE app_code = ? AND user_id IN (`+placeholders(len(ids))+`)
|
||||||
|
`, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var user grantUserDTO
|
||||||
|
if err := rows.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out[user.UserID] = user
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyGrantTargetUsers(grants []grantDTO, users map[int64]grantUserDTO) {
|
||||||
|
for index := range grants {
|
||||||
|
targetUserID := grants[index].TargetUserID
|
||||||
|
if targetUserID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if user, ok := users[targetUserID]; ok {
|
||||||
|
grants[index].TargetUser = &user
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
grants[index].TargetUser = &grantUserDTO{UserID: targetUserID}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) enrichGrantOperators(ctx context.Context, grants []grantDTO) error {
|
func (h *Handler) enrichGrantOperators(ctx context.Context, grants []grantDTO) error {
|
||||||
// operator_user_id 在 wallet 发放事实里只保存稳定 ID;后台列表按来源补展示资料,避免把 admin 用户和 app 用户混成同一张表。
|
// operator_user_id 在 wallet 发放事实里只保存稳定 ID;后台列表按来源补展示资料,避免把 admin 用户和 app 用户混成同一张表。
|
||||||
adminIDs, managerIDs := collectGrantOperatorIDs(grants)
|
adminIDs, managerIDs := collectGrantOperatorIDs(grants)
|
||||||
@ -140,6 +200,21 @@ func collectGrantOperatorIDs(grants []grantDTO) ([]uint, []int64) {
|
|||||||
return adminIDs, managerIDs
|
return adminIDs, managerIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectGrantTargetIDs(grants []grantDTO) []int64 {
|
||||||
|
targetSet := make(map[int64]struct{})
|
||||||
|
for _, grant := range grants {
|
||||||
|
if grant.TargetUserID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetSet[grant.TargetUserID] = struct{}{}
|
||||||
|
}
|
||||||
|
targetIDs := make([]int64, 0, len(targetSet))
|
||||||
|
for id := range targetSet {
|
||||||
|
targetIDs = append(targetIDs, id)
|
||||||
|
}
|
||||||
|
return targetIDs
|
||||||
|
}
|
||||||
|
|
||||||
func adminOperatorFromModel(user model.User) adminOperator {
|
func adminOperatorFromModel(user model.User) adminOperator {
|
||||||
return adminOperator{
|
return adminOperator{
|
||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package resource
|
package resource
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestApplyGrantOperatorsUsesSourceSpecificProfiles(t *testing.T) {
|
func TestApplyGrantOperatorsUsesSourceSpecificProfiles(t *testing.T) {
|
||||||
grants := []grantDTO{
|
grants := []grantDTO{
|
||||||
@ -22,22 +25,48 @@ func TestApplyGrantOperatorsUsesSourceSpecificProfiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyGrantTargetUsersKeepsFallbackUserID(t *testing.T) {
|
||||||
|
grants := []grantDTO{
|
||||||
|
{GrantID: "grant-with-user", TargetUserID: 1001},
|
||||||
|
{GrantID: "grant-missing-user", TargetUserID: 1002},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGrantTargetUsers(grants, map[int64]grantUserDTO{
|
||||||
|
1001: {UserID: 1001, DisplayUserID: "900001", Username: "玩家 A", Avatar: "https://cdn.example/user.png"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if grants[0].TargetUser == nil || grants[0].TargetUser.DisplayUserID != "900001" || grants[0].TargetUser.Username != "玩家 A" {
|
||||||
|
t.Fatalf("target user mismatch: %+v", grants[0].TargetUser)
|
||||||
|
}
|
||||||
|
if grants[1].TargetUser == nil || grants[1].TargetUser.UserID != 1002 || grants[1].TargetUser.DisplayUserID != "" {
|
||||||
|
t.Fatalf("target fallback mismatch: %+v", grants[1].TargetUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEmojiPackMetadataDefaultsToAllRegions(t *testing.T) {
|
func TestEmojiPackMetadataDefaultsToAllRegions(t *testing.T) {
|
||||||
metadata, err := emojiPackMetadataJSON([]int64{0, 1001})
|
metadata, err := emojiPackMetadataJSON([]int64{0, 1001}, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("metadata build failed: %v", err)
|
t.Fatalf("metadata build failed: %v", err)
|
||||||
}
|
}
|
||||||
if metadata != `{"region_scope":"all"}` {
|
var payload emojiPackMetadataPayload
|
||||||
t.Fatalf("unexpected all-region metadata: %s", metadata)
|
if err := json.Unmarshal([]byte(metadata), &payload); err != nil {
|
||||||
|
t.Fatalf("metadata json mismatch: %v", err)
|
||||||
|
}
|
||||||
|
if payload.RegionScope != "all" || len(payload.RegionIDs) != 0 || payload.Category != defaultEmojiPackCategory || payload.PricingType != emojiPackPricingFree {
|
||||||
|
t.Fatalf("unexpected all-region metadata: %+v", payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmojiPackMetadataKeepsLimitedRegions(t *testing.T) {
|
func TestEmojiPackMetadataKeepsLimitedRegions(t *testing.T) {
|
||||||
metadata, err := emojiPackMetadataJSON([]int64{1001, 1001, 1002})
|
metadata, err := emojiPackMetadataJSON([]int64{1001, 1001, 1002}, "会员", "paid")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("metadata build failed: %v", err)
|
t.Fatalf("metadata build failed: %v", err)
|
||||||
}
|
}
|
||||||
if metadata != `{"region_ids":[1001,1002],"region_scope":"limited"}` {
|
payload := emojiPackMetadataFromJSON(metadata)
|
||||||
t.Fatalf("unexpected limited-region metadata: %s", metadata)
|
if payload.RegionScope != "limited" || payload.Category != "会员" || payload.PricingType != emojiPackPricingPaid {
|
||||||
|
t.Fatalf("unexpected limited-region metadata: %+v", payload)
|
||||||
|
}
|
||||||
|
if len(payload.RegionIDs) != 2 || payload.RegionIDs[0] != 1001 || payload.RegionIDs[1] != 1002 {
|
||||||
|
t.Fatalf("unexpected region ids: %+v", payload.RegionIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
"hyapp-admin-server/internal/middleware"
|
"hyapp-admin-server/internal/middleware"
|
||||||
@ -20,6 +21,12 @@ const (
|
|||||||
resourceTypeEmojiPack = "emoji_pack"
|
resourceTypeEmojiPack = "emoji_pack"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultEmojiPackCategory = "默认"
|
||||||
|
emojiPackPricingFree = "free"
|
||||||
|
emojiPackPricingPaid = "paid"
|
||||||
|
)
|
||||||
|
|
||||||
type resourceRequest struct {
|
type resourceRequest struct {
|
||||||
ResourceCode string `json:"resourceCode"`
|
ResourceCode string `json:"resourceCode"`
|
||||||
ResourceType string `json:"resourceType"`
|
ResourceType string `json:"resourceType"`
|
||||||
@ -35,12 +42,21 @@ type resourceRequest struct {
|
|||||||
|
|
||||||
type emojiPackRequest struct {
|
type emojiPackRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricingType"`
|
||||||
CoverURL string `json:"coverUrl"`
|
CoverURL string `json:"coverUrl"`
|
||||||
AnimationURL string `json:"animationUrl"`
|
AnimationURL string `json:"animationUrl"`
|
||||||
RegionIDs []int64 `json:"regionIds"`
|
RegionIDs []int64 `json:"regionIds"`
|
||||||
SortOrder int32 `json:"sortOrder"`
|
SortOrder int32 `json:"sortOrder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type emojiPackMetadataPayload struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricing_type"`
|
||||||
|
RegionIDs []int64 `json:"region_ids,omitempty"`
|
||||||
|
RegionScope string `json:"region_scope"`
|
||||||
|
}
|
||||||
|
|
||||||
type resourceGroupRequest struct {
|
type resourceGroupRequest struct {
|
||||||
GroupCode string `json:"groupCode"`
|
GroupCode string `json:"groupCode"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@ -153,7 +169,11 @@ func (r emojiPackRequest) createProto(c *gin.Context) (*walletv1.CreateResourceR
|
|||||||
if name == "" || coverURL == "" || animationURL == "" {
|
if name == "" || coverURL == "" || animationURL == "" {
|
||||||
return nil, fmt.Errorf("表情包参数不完整")
|
return nil, fmt.Errorf("表情包参数不完整")
|
||||||
}
|
}
|
||||||
metadataJSON, err := emojiPackMetadataJSON(r.RegionIDs)
|
category := normalizeEmojiPackCategory(r.Category)
|
||||||
|
if utf8.RuneCountInString(category) > 64 {
|
||||||
|
return nil, fmt.Errorf("表情包分类不能超过 64 个字符")
|
||||||
|
}
|
||||||
|
metadataJSON, err := emojiPackMetadataJSON(r.RegionIDs, category, r.PricingType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -313,20 +333,62 @@ func resourceGrantStrategy(resourceType string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func emojiPackMetadataJSON(regionIDs []int64) (string, error) {
|
func emojiPackMetadataJSON(regionIDs []int64, category string, pricingType string) (string, error) {
|
||||||
normalizedRegionIDs := normalizeRegionIDs(regionIDs)
|
normalizedRegionIDs := normalizeRegionIDs(regionIDs)
|
||||||
metadata := map[string]any{"region_scope": "all"}
|
metadata := emojiPackMetadataPayload{
|
||||||
|
Category: normalizeEmojiPackCategory(category),
|
||||||
|
PricingType: normalizeEmojiPackPricingType(pricingType),
|
||||||
|
RegionScope: "all",
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(metadata.Category) > 64 {
|
||||||
|
return "", fmt.Errorf("表情包分类不能超过 64 个字符")
|
||||||
|
}
|
||||||
if len(normalizedRegionIDs) > 0 {
|
if len(normalizedRegionIDs) > 0 {
|
||||||
metadata["region_scope"] = "limited"
|
metadata.RegionScope = "limited"
|
||||||
metadata["region_ids"] = normalizedRegionIDs
|
metadata.RegionIDs = normalizedRegionIDs
|
||||||
}
|
}
|
||||||
body, err := json.Marshal(metadata)
|
body, err := json.Marshal(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("表情包区域配置不正确")
|
return "", fmt.Errorf("表情包配置不正确")
|
||||||
}
|
}
|
||||||
return string(body), nil
|
return string(body), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadataPayload {
|
||||||
|
payload := emojiPackMetadataPayload{
|
||||||
|
Category: defaultEmojiPackCategory,
|
||||||
|
PricingType: emojiPackPricingFree,
|
||||||
|
RegionScope: "all",
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
payload.Category = normalizeEmojiPackCategory(payload.Category)
|
||||||
|
payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType)
|
||||||
|
payload.RegionIDs = normalizeRegionIDs(payload.RegionIDs)
|
||||||
|
if len(payload.RegionIDs) > 0 {
|
||||||
|
payload.RegionScope = "limited"
|
||||||
|
} else {
|
||||||
|
payload.RegionScope = "all"
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmojiPackCategory(value string) string {
|
||||||
|
category := strings.TrimSpace(value)
|
||||||
|
if category == "" {
|
||||||
|
return defaultEmojiPackCategory
|
||||||
|
}
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmojiPackPricingType(value string) string {
|
||||||
|
if strings.ToLower(strings.TrimSpace(value)) == emojiPackPricingPaid {
|
||||||
|
return emojiPackPricingPaid
|
||||||
|
}
|
||||||
|
return emojiPackPricingFree
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeRegionIDs(regionIDs []int64) []int64 {
|
func normalizeRegionIDs(regionIDs []int64) []int64 {
|
||||||
out := make([]int64, 0, len(regionIDs))
|
out := make([]int64, 0, len(regionIDs))
|
||||||
seen := make(map[int64]struct{}, len(regionIDs))
|
seen := make(map[int64]struct{}, len(regionIDs))
|
||||||
|
|||||||
@ -19,6 +19,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
|||||||
protected.POST("/admin/resources/:resource_id/disable", middleware.RequirePermission("resource:update"), h.DisableResource)
|
protected.POST("/admin/resources/:resource_id/disable", middleware.RequirePermission("resource:update"), h.DisableResource)
|
||||||
|
|
||||||
protected.GET("/admin/emoji-packs", middleware.RequirePermission("emoji-pack:view"), h.ListEmojiPacks)
|
protected.GET("/admin/emoji-packs", middleware.RequirePermission("emoji-pack:view"), h.ListEmojiPacks)
|
||||||
|
protected.GET("/admin/emoji-packs/categories", middleware.RequirePermission("emoji-pack:view"), h.ListEmojiPackCategories)
|
||||||
protected.POST("/admin/emoji-packs", middleware.RequirePermission("emoji-pack:create"), h.CreateEmojiPack)
|
protected.POST("/admin/emoji-packs", middleware.RequirePermission("emoji-pack:create"), h.CreateEmojiPack)
|
||||||
|
|
||||||
protected.GET("/admin/resource-groups", middleware.RequirePermission("resource-group:view"), h.ListResourceGroups)
|
protected.GET("/admin/resource-groups", middleware.RequirePermission("resource-group:view"), h.ListResourceGroups)
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"hyapp-admin-server/internal/integration/roomclient"
|
||||||
|
"hyapp-admin-server/internal/middleware"
|
||||||
"hyapp-admin-server/internal/modules/shared"
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
"hyapp-admin-server/internal/response"
|
"hyapp-admin-server/internal/response"
|
||||||
|
|
||||||
@ -17,8 +19,8 @@ type Handler struct {
|
|||||||
audit shared.OperationLogger
|
audit shared.OperationLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(roomDB *sql.DB, userDB *sql.DB, audit shared.OperationLogger) *Handler {
|
func New(roomDB *sql.DB, userDB *sql.DB, roomClient roomclient.Client, audit shared.OperationLogger) *Handler {
|
||||||
return &Handler{service: NewService(roomDB, userDB), audit: audit}
|
return &Handler{service: NewService(roomDB, userDB, roomClient), audit: audit}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) ListRooms(c *gin.Context) {
|
func (h *Handler) ListRooms(c *gin.Context) {
|
||||||
@ -110,7 +112,7 @@ func (h *Handler) UpdateRoom(c *gin.Context) {
|
|||||||
response.BadRequest(c, "房间参数不正确")
|
response.BadRequest(c, "房间参数不正确")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
room, err := h.service.UpdateRoom(c.Request.Context(), roomID, req)
|
room, err := h.service.UpdateRoom(c.Request.Context(), roomID, req, shared.ActorFromContext(c), middleware.CurrentRequestID(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeMutationError(c, err, "更新房间失败")
|
writeMutationError(c, err, "更新房间失败")
|
||||||
return
|
return
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type createRoomPinRequest struct {
|
|||||||
|
|
||||||
type updateRoomRequest struct {
|
type updateRoomRequest struct {
|
||||||
CoverURL *string `json:"coverUrl"`
|
CoverURL *string `json:"coverUrl"`
|
||||||
|
CloseReason *string `json:"closeReason"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Mode *string `json:"mode"`
|
Mode *string `json:"mode"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
|
|||||||
@ -9,8 +9,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"hyapp-admin-server/internal/appctx"
|
"hyapp-admin-server/internal/appctx"
|
||||||
|
"hyapp-admin-server/internal/integration/roomclient"
|
||||||
|
"hyapp-admin-server/internal/modules/shared"
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,8 +24,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
userDB *sql.DB
|
userDB *sql.DB
|
||||||
|
roomClient roomclient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomOwner struct {
|
type RoomOwner struct {
|
||||||
@ -33,6 +38,10 @@ type RoomOwner struct {
|
|||||||
|
|
||||||
type Room struct {
|
type Room struct {
|
||||||
CoverURL string `json:"coverUrl"`
|
CoverURL string `json:"coverUrl"`
|
||||||
|
ClosedAtMs int64 `json:"closedAtMs,omitempty"`
|
||||||
|
ClosedByAdminID uint `json:"closedByAdminId,omitempty"`
|
||||||
|
ClosedByAdminName string `json:"closedByAdminName,omitempty"`
|
||||||
|
CloseReason string `json:"closeReason,omitempty"`
|
||||||
CreatedAtMs int64 `json:"createdAtMs"`
|
CreatedAtMs int64 `json:"createdAtMs"`
|
||||||
Heat int64 `json:"heat"`
|
Heat int64 `json:"heat"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
@ -52,8 +61,8 @@ type Room struct {
|
|||||||
VisibleRegionID int64 `json:"visibleRegionId"`
|
VisibleRegionID int64 `json:"visibleRegionId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(db *sql.DB, userDB *sql.DB) *Service {
|
func NewService(db *sql.DB, userDB *sql.DB, roomClient roomclient.Client) *Service {
|
||||||
return &Service{db: db, userDB: userDB}
|
return &Service{db: db, userDB: userDB, roomClient: roomClient}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64, error) {
|
func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64, error) {
|
||||||
@ -93,6 +102,10 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
|||||||
rle.cover_url,
|
rle.cover_url,
|
||||||
rle.mode,
|
rle.mode,
|
||||||
rle.status,
|
rle.status,
|
||||||
|
rle.close_reason,
|
||||||
|
rle.closed_by_admin_id,
|
||||||
|
rle.closed_by_admin_name,
|
||||||
|
rle.closed_at_ms,
|
||||||
rle.heat,
|
rle.heat,
|
||||||
rle.online_count,
|
rle.online_count,
|
||||||
rle.seat_count,
|
rle.seat_count,
|
||||||
@ -140,6 +153,10 @@ func (s *Service) GetRoom(ctx context.Context, roomID string) (Room, error) {
|
|||||||
cover_url,
|
cover_url,
|
||||||
mode,
|
mode,
|
||||||
status,
|
status,
|
||||||
|
close_reason,
|
||||||
|
closed_by_admin_id,
|
||||||
|
closed_by_admin_name,
|
||||||
|
closed_at_ms,
|
||||||
heat,
|
heat,
|
||||||
online_count,
|
online_count,
|
||||||
seat_count,
|
seat_count,
|
||||||
@ -164,7 +181,7 @@ func (s *Service) GetRoom(ctx context.Context, roomID string) (Room, error) {
|
|||||||
return items[0], nil
|
return items[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomRequest) (Room, error) {
|
func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomRequest, actor shared.Actor, requestID string) (Room, error) {
|
||||||
if s.db == nil {
|
if s.db == nil {
|
||||||
return Room{}, fmt.Errorf("room mysql is not configured")
|
return Room{}, fmt.Errorf("room mysql is not configured")
|
||||||
}
|
}
|
||||||
@ -175,6 +192,15 @@ func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomR
|
|||||||
if len(updates) == 0 {
|
if len(updates) == 0 {
|
||||||
return s.GetRoom(ctx, roomID)
|
return s.GetRoom(ctx, roomID)
|
||||||
}
|
}
|
||||||
|
if snapshotPatch.statusSet && snapshotPatch.status == "closed" {
|
||||||
|
if err := s.closeRoomLifecycle(ctx, roomID, actor, requestID); err != nil {
|
||||||
|
return Room{}, err
|
||||||
|
}
|
||||||
|
} else if snapshotPatch.statusSet && snapshotPatch.status == "active" {
|
||||||
|
if err := s.reopenRoomLifecycle(ctx, roomID, actor, requestID); err != nil {
|
||||||
|
return Room{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -192,10 +218,10 @@ func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomR
|
|||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
listSets := make([]string, 0, len(updates)+1)
|
listSets := make([]string, 0, len(updates)+5)
|
||||||
listArgs := make([]any, 0, len(updates)+2)
|
listArgs := make([]any, 0, len(updates)+6)
|
||||||
roomSets := make([]string, 0, 3)
|
roomSets := make([]string, 0, len(updates)+4)
|
||||||
roomArgs := make([]any, 0, 4)
|
roomArgs := make([]any, 0, len(updates)+6)
|
||||||
for _, update := range updates {
|
for _, update := range updates {
|
||||||
if update.listColumn != "" {
|
if update.listColumn != "" {
|
||||||
listSets = append(listSets, update.listColumn+" = ?")
|
listSets = append(listSets, update.listColumn+" = ?")
|
||||||
@ -206,6 +232,27 @@ func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomR
|
|||||||
roomArgs = append(roomArgs, update.value)
|
roomArgs = append(roomArgs, update.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if snapshotPatch.statusSet {
|
||||||
|
if snapshotPatch.status == "closed" {
|
||||||
|
reason := strings.TrimSpace(valueOrEmpty(req.CloseReason))
|
||||||
|
operatorName := strings.TrimSpace(actor.Username)
|
||||||
|
closeUpdates := []roomUpdate{
|
||||||
|
{listColumn: "close_reason", roomColumn: "close_reason", value: reason},
|
||||||
|
{listColumn: "closed_by_admin_id", roomColumn: "closed_by_admin_id", value: actor.UserID},
|
||||||
|
{listColumn: "closed_by_admin_name", roomColumn: "closed_by_admin_name", value: operatorName},
|
||||||
|
{listColumn: "closed_at_ms", roomColumn: "closed_at_ms", value: now},
|
||||||
|
}
|
||||||
|
appendRoomUpdates(closeUpdates, &listSets, &listArgs, &roomSets, &roomArgs)
|
||||||
|
} else {
|
||||||
|
clearUpdates := []roomUpdate{
|
||||||
|
{listColumn: "close_reason", roomColumn: "close_reason", value: ""},
|
||||||
|
{listColumn: "closed_by_admin_id", roomColumn: "closed_by_admin_id", value: 0},
|
||||||
|
{listColumn: "closed_by_admin_name", roomColumn: "closed_by_admin_name", value: ""},
|
||||||
|
{listColumn: "closed_at_ms", roomColumn: "closed_at_ms", value: 0},
|
||||||
|
}
|
||||||
|
appendRoomUpdates(clearUpdates, &listSets, &listArgs, &roomSets, &roomArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
listSets = append(listSets, "updated_at_ms = ?")
|
listSets = append(listSets, "updated_at_ms = ?")
|
||||||
listArgs = append(listArgs, now, appCode, roomID)
|
listArgs = append(listArgs, now, appCode, roomID)
|
||||||
if _, err := tx.ExecContext(ctx, "UPDATE room_list_entries SET "+strings.Join(listSets, ", ")+" WHERE app_code = ? AND room_id = ?", listArgs...); err != nil {
|
if _, err := tx.ExecContext(ctx, "UPDATE room_list_entries SET "+strings.Join(listSets, ", ")+" WHERE app_code = ? AND room_id = ?", listArgs...); err != nil {
|
||||||
@ -226,6 +273,72 @@ func (s *Service) UpdateRoom(ctx context.Context, roomID string, req updateRoomR
|
|||||||
return s.GetRoom(ctx, roomID)
|
return s.GetRoom(ctx, roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) closeRoomLifecycle(ctx context.Context, roomID string, actor shared.Actor, requestID string) error {
|
||||||
|
if s.roomClient == nil {
|
||||||
|
return fmt.Errorf("room service client is not configured")
|
||||||
|
}
|
||||||
|
if actor.UserID == 0 {
|
||||||
|
return fmt.Errorf("%w: 操作人不正确", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
_, err := s.roomClient.CloseRoom(ctx, roomclient.CloseRoomRequest{
|
||||||
|
RequestID: requestID,
|
||||||
|
RoomID: roomID,
|
||||||
|
ActorUserID: int64(actor.UserID),
|
||||||
|
Reason: "admin_closed",
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if st, ok := status.FromError(err); ok {
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.NotFound:
|
||||||
|
return ErrNotFound
|
||||||
|
case codes.InvalidArgument, codes.FailedPrecondition:
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidArgument, st.Message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) reopenRoomLifecycle(ctx context.Context, roomID string, actor shared.Actor, requestID string) error {
|
||||||
|
if s.roomClient == nil {
|
||||||
|
return fmt.Errorf("room service client is not configured")
|
||||||
|
}
|
||||||
|
if actor.UserID == 0 {
|
||||||
|
return fmt.Errorf("%w: 操作人不正确", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
_, err := s.roomClient.ReopenRoom(ctx, roomclient.ReopenRoomRequest{
|
||||||
|
RequestID: requestID,
|
||||||
|
RoomID: roomID,
|
||||||
|
ActorUserID: int64(actor.UserID),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if st, ok := status.FromError(err); ok {
|
||||||
|
switch st.Code() {
|
||||||
|
case codes.NotFound:
|
||||||
|
return ErrNotFound
|
||||||
|
case codes.InvalidArgument, codes.FailedPrecondition:
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidArgument, st.Message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendRoomUpdates(updates []roomUpdate, listSets *[]string, listArgs *[]any, roomSets *[]string, roomArgs *[]any) {
|
||||||
|
for _, update := range updates {
|
||||||
|
if update.listColumn != "" {
|
||||||
|
*listSets = append(*listSets, update.listColumn+" = ?")
|
||||||
|
*listArgs = append(*listArgs, update.value)
|
||||||
|
}
|
||||||
|
if update.roomColumn != "" {
|
||||||
|
*roomSets = append(*roomSets, update.roomColumn+" = ?")
|
||||||
|
*roomArgs = append(*roomArgs, update.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteRoom(ctx context.Context, roomID string) error {
|
func (s *Service) DeleteRoom(ctx context.Context, roomID string) error {
|
||||||
if s.db == nil {
|
if s.db == nil {
|
||||||
return fmt.Errorf("room mysql is not configured")
|
return fmt.Errorf("room mysql is not configured")
|
||||||
@ -327,6 +440,15 @@ func buildRoomUpdate(req updateRoomRequest) ([]roomUpdate, snapshotPatch, error)
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, patch, fmt.Errorf("%w: 房间状态不正确", ErrInvalidArgument)
|
return nil, patch, fmt.Errorf("%w: 房间状态不正确", ErrInvalidArgument)
|
||||||
}
|
}
|
||||||
|
if value == "closed" {
|
||||||
|
reason := strings.TrimSpace(valueOrEmpty(req.CloseReason))
|
||||||
|
if reason == "" {
|
||||||
|
return nil, patch, fmt.Errorf("%w: 请输入关闭原因", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
if len(reason) > 512 {
|
||||||
|
return nil, patch, fmt.Errorf("%w: 关闭原因不能超过 512 个字符", ErrInvalidArgument)
|
||||||
|
}
|
||||||
|
}
|
||||||
updates = append(updates, roomUpdate{listColumn: "status", roomColumn: "status", value: value})
|
updates = append(updates, roomUpdate{listColumn: "status", roomColumn: "status", value: value})
|
||||||
patch.statusSet = true
|
patch.statusSet = true
|
||||||
patch.status = value
|
patch.status = value
|
||||||
@ -390,6 +512,7 @@ type roomScanner interface {
|
|||||||
func scanRoom(row roomScanner) (Room, error) {
|
func scanRoom(row roomScanner) (Room, error) {
|
||||||
var item Room
|
var item Room
|
||||||
var ownerUserID int64
|
var ownerUserID int64
|
||||||
|
var closedByAdminID uint64
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&item.RoomID,
|
&item.RoomID,
|
||||||
&item.RoomShortID,
|
&item.RoomShortID,
|
||||||
@ -399,6 +522,10 @@ func scanRoom(row roomScanner) (Room, error) {
|
|||||||
&item.CoverURL,
|
&item.CoverURL,
|
||||||
&item.Mode,
|
&item.Mode,
|
||||||
&item.Status,
|
&item.Status,
|
||||||
|
&item.CloseReason,
|
||||||
|
&closedByAdminID,
|
||||||
|
&item.ClosedByAdminName,
|
||||||
|
&item.ClosedAtMs,
|
||||||
&item.Heat,
|
&item.Heat,
|
||||||
&item.OnlineCount,
|
&item.OnlineCount,
|
||||||
&item.SeatCount,
|
&item.SeatCount,
|
||||||
@ -407,6 +534,7 @@ func scanRoom(row roomScanner) (Room, error) {
|
|||||||
&item.CreatedAtMs,
|
&item.CreatedAtMs,
|
||||||
&item.UpdatedAtMs,
|
&item.UpdatedAtMs,
|
||||||
)
|
)
|
||||||
|
item.ClosedByAdminID = uint(closedByAdminID)
|
||||||
item.OwnerUserID = strconv.FormatInt(ownerUserID, 10)
|
item.OwnerUserID = strconv.FormatInt(ownerUserID, 10)
|
||||||
item.Owner = RoomOwner{UserID: item.OwnerUserID}
|
item.Owner = RoomOwner{UserID: item.OwnerUserID}
|
||||||
item.RoomContribution = item.Heat
|
item.RoomContribution = item.Heat
|
||||||
@ -480,6 +608,13 @@ func normalizeRoomStatus(status string) (string, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func valueOrEmpty(value *string) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *value
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) fillRoomDetails(ctx context.Context, items []Room) error {
|
func (s *Service) fillRoomDetails(ctx context.Context, items []Room) error {
|
||||||
if s.userDB == nil || len(items) == 0 {
|
if s.userDB == nil || len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -22,6 +22,8 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "App 用户设置密码", Code: "app-user:password", Kind: "button"},
|
{Name: "App 用户设置密码", Code: "app-user:password", Kind: "button"},
|
||||||
{Name: "等级配置查看", Code: "level-config:view", Kind: "menu"},
|
{Name: "等级配置查看", Code: "level-config:view", Kind: "menu"},
|
||||||
{Name: "等级配置更新", Code: "level-config:update", Kind: "button"},
|
{Name: "等级配置更新", Code: "level-config:update", Kind: "button"},
|
||||||
|
{Name: "地区屏蔽查看", Code: "region-block:view", Kind: "menu"},
|
||||||
|
{Name: "地区屏蔽更新", Code: "region-block:update", Kind: "button"},
|
||||||
{Name: "房间查看", Code: "room:view", Kind: "menu"},
|
{Name: "房间查看", Code: "room:view", Kind: "menu"},
|
||||||
{Name: "房间更新", Code: "room:update", Kind: "button"},
|
{Name: "房间更新", Code: "room:update", Kind: "button"},
|
||||||
{Name: "房间删除", Code: "room:delete", Kind: "button"},
|
{Name: "房间删除", Code: "room:delete", Kind: "button"},
|
||||||
@ -204,6 +206,7 @@ func (s *Store) seedMenus() error {
|
|||||||
{ParentID: &appUsersID, Title: "封禁列表", Code: "app-user-bans", Path: "/app/users/bans", Icon: "block", PermissionCode: "app-user:view", Sort: 61, Visible: true},
|
{ParentID: &appUsersID, Title: "封禁列表", Code: "app-user-bans", Path: "/app/users/bans", Icon: "block", PermissionCode: "app-user:view", Sort: 61, Visible: true},
|
||||||
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 62, Visible: true},
|
{ParentID: &appUsersID, Title: "登录日志", Code: "app-user-login-logs", Path: "/app/users/login-logs", Icon: "login", PermissionCode: "app-user:view", Sort: 62, Visible: true},
|
||||||
{ParentID: &appUsersID, Title: "等级配置", Code: "app-user-level-config", Path: "/app/users/level-config", Icon: "military_tech", PermissionCode: "level-config:view", Sort: 63, Visible: true},
|
{ParentID: &appUsersID, Title: "等级配置", Code: "app-user-level-config", Path: "/app/users/level-config", Icon: "military_tech", PermissionCode: "level-config:view", Sort: 63, Visible: true},
|
||||||
|
{ParentID: &appUsersID, Title: "地区屏蔽", Code: "app-user-region-blocks", Path: "/app/users/region-blocks", Icon: "public", PermissionCode: "region-block:view", Sort: 64, Visible: true},
|
||||||
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
|
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
|
||||||
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
|
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
|
||||||
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
|
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
|
||||||
@ -441,6 +444,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"user:view", "user:status",
|
"user:view", "user:status",
|
||||||
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
|
"app-user:view", "app-user:update", "app-user:status", "app-user:password",
|
||||||
"level-config:view", "level-config:update",
|
"level-config:view", "level-config:update",
|
||||||
|
"region-block:view", "region-block:update",
|
||||||
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
||||||
"app-config:view", "app-config:update",
|
"app-config:view", "app-config:update",
|
||||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||||
@ -464,13 +468,14 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"upload:create",
|
"upload:create",
|
||||||
}
|
}
|
||||||
case "auditor":
|
case "auditor":
|
||||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "role:view", "permission:view", "job:view"}
|
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "role:view", "permission:view", "job:view"}
|
||||||
case "readonly":
|
case "readonly":
|
||||||
return []string{
|
return []string{
|
||||||
"overview:view",
|
"overview:view",
|
||||||
"user:view",
|
"user:view",
|
||||||
"app-user:view",
|
"app-user:view",
|
||||||
"level-config:view",
|
"level-config:view",
|
||||||
|
"region-block:view",
|
||||||
"room:view",
|
"room:view",
|
||||||
"room-pin:view",
|
"room-pin:view",
|
||||||
"room-config:view",
|
"room-config:view",
|
||||||
@ -514,6 +519,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"app-config:view", "app-config:update",
|
"app-config:view", "app-config:update",
|
||||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||||
"level-config:view", "level-config:update",
|
"level-config:view", "level-config:update",
|
||||||
|
"region-block:view", "region-block:update",
|
||||||
"resource:view", "resource:create", "resource:update",
|
"resource:view", "resource:create", "resource:update",
|
||||||
"resource-group:view", "resource-group:create", "resource-group:update",
|
"resource-group:view", "resource-group:create", "resource-group:update",
|
||||||
"resource-grant:view", "resource-grant:create",
|
"resource-grant:view", "resource-grant:create",
|
||||||
@ -529,7 +535,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"achievement:view", "achievement:create", "achievement:update",
|
"achievement:view", "achievement:create", "achievement:update",
|
||||||
}
|
}
|
||||||
case "auditor", "readonly":
|
case "auditor", "readonly":
|
||||||
return []string{"level-config:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view"}
|
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view"}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"hyapp-admin-server/internal/modules/menu"
|
"hyapp-admin-server/internal/modules/menu"
|
||||||
"hyapp-admin-server/internal/modules/payment"
|
"hyapp-admin-server/internal/modules/payment"
|
||||||
"hyapp-admin-server/internal/modules/rbac"
|
"hyapp-admin-server/internal/modules/rbac"
|
||||||
|
"hyapp-admin-server/internal/modules/regionblock"
|
||||||
"hyapp-admin-server/internal/modules/registrationreward"
|
"hyapp-admin-server/internal/modules/registrationreward"
|
||||||
resourcemodule "hyapp-admin-server/internal/modules/resource"
|
resourcemodule "hyapp-admin-server/internal/modules/resource"
|
||||||
"hyapp-admin-server/internal/modules/roomadmin"
|
"hyapp-admin-server/internal/modules/roomadmin"
|
||||||
@ -54,6 +55,7 @@ type Handlers struct {
|
|||||||
Payment *payment.Handler
|
Payment *payment.Handler
|
||||||
RBAC *rbac.Handler
|
RBAC *rbac.Handler
|
||||||
RegistrationReward *registrationreward.Handler
|
RegistrationReward *registrationreward.Handler
|
||||||
|
RegionBlock *regionblock.Handler
|
||||||
Resource *resourcemodule.Handler
|
Resource *resourcemodule.Handler
|
||||||
RoomAdmin *roomadmin.Handler
|
RoomAdmin *roomadmin.Handler
|
||||||
Search *search.Handler
|
Search *search.Handler
|
||||||
@ -84,6 +86,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
|
|||||||
countryregion.RegisterRoutes(protected, h.CountryRegion)
|
countryregion.RegisterRoutes(protected, h.CountryRegion)
|
||||||
dailytask.RegisterRoutes(protected, h.DailyTask)
|
dailytask.RegisterRoutes(protected, h.DailyTask)
|
||||||
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
registrationreward.RegisterRoutes(protected, h.RegistrationReward)
|
||||||
|
regionblock.RegisterRoutes(protected, h.RegionBlock)
|
||||||
gamemanagement.RegisterRoutes(protected, h.Game)
|
gamemanagement.RegisterRoutes(protected, h.Game)
|
||||||
roomadmin.RegisterRoutes(protected, h.RoomAdmin)
|
roomadmin.RegisterRoutes(protected, h.RoomAdmin)
|
||||||
dashboard.RegisterRoutes(protected, h.Dashboard)
|
dashboard.RegisterRoutes(protected, h.Dashboard)
|
||||||
|
|||||||
58
server/admin/migrations/014_region_block_navigation.sql
Normal file
58
server/admin/migrations/014_region_block_navigation.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- Region block is a user-service login risk policy maintained from User Management.
|
||||||
|
-- The migration seeds menu and RBAC facts for environments that do not run bootstrap.
|
||||||
|
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||||
|
|
||||||
|
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||||
|
('地区屏蔽查看', 'region-block:view', 'menu', '', @now_ms, @now_ms),
|
||||||
|
('地区屏蔽更新', 'region-block:update', 'button', '', @now_ms, @now_ms)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
kind = VALUES(kind),
|
||||||
|
description = VALUES(description),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
|
||||||
|
(NULL, '用户管理', 'app-users', '', 'users', '', 60, TRUE, @now_ms, @now_ms)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
title = VALUES(title),
|
||||||
|
path = VALUES(path),
|
||||||
|
icon = VALUES(icon),
|
||||||
|
permission_code = VALUES(permission_code),
|
||||||
|
sort = VALUES(sort),
|
||||||
|
visible = VALUES(visible),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||||
|
SELECT parent.id, '地区屏蔽', 'app-user-region-blocks', '/app/users/region-blocks', 'public', 'region-block:view', 64, TRUE, @now_ms, @now_ms
|
||||||
|
FROM admin_menus parent
|
||||||
|
WHERE parent.code = 'app-users'
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
parent_id = VALUES(parent_id),
|
||||||
|
title = VALUES(title),
|
||||||
|
path = VALUES(path),
|
||||||
|
icon = VALUES(icon),
|
||||||
|
permission_code = VALUES(permission_code),
|
||||||
|
sort = VALUES(sort),
|
||||||
|
visible = VALUES(visible),
|
||||||
|
updated_at_ms = @now_ms;
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT admin_role.id, admin_permission.id
|
||||||
|
FROM admin_roles admin_role
|
||||||
|
JOIN admin_permissions admin_permission
|
||||||
|
WHERE admin_role.code = 'platform-admin'
|
||||||
|
AND admin_permission.code IN ('region-block:view', 'region-block:update');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT admin_role.id, admin_permission.id
|
||||||
|
FROM admin_roles admin_role
|
||||||
|
JOIN admin_permissions admin_permission
|
||||||
|
WHERE admin_role.code = 'ops-admin'
|
||||||
|
AND admin_permission.code IN ('region-block:view', 'region-block:update');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||||
|
SELECT admin_role.id, admin_permission.id
|
||||||
|
FROM admin_roles admin_role
|
||||||
|
JOIN admin_permissions admin_permission
|
||||||
|
WHERE admin_role.code IN ('auditor', 'readonly')
|
||||||
|
AND admin_permission.code = 'region-block:view';
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/activity-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/activity-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/cron-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/cron-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@ -7,10 +8,14 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/game-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/game-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
ENV TZ=UTC
|
ENV TZ=UTC
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,11 +10,15 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/gateway-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/gateway-service/cmd/server
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,7 @@ type UserDeviceClient interface {
|
|||||||
// UserCountryQueryClient 抽象 gateway 对 App 注册页国家列表的公开读依赖。
|
// UserCountryQueryClient 抽象 gateway 对 App 注册页国家列表的公开读依赖。
|
||||||
type UserCountryQueryClient interface {
|
type UserCountryQueryClient interface {
|
||||||
ListRegistrationCountries(ctx context.Context, req *userv1.ListRegistrationCountriesRequest) (*userv1.ListRegistrationCountriesResponse, error)
|
ListRegistrationCountries(ctx context.Context, req *userv1.ListRegistrationCountriesRequest) (*userv1.ListRegistrationCountriesResponse, error)
|
||||||
|
ListLoginRiskBlockedCountries(ctx context.Context, req *userv1.ListLoginRiskBlockedCountriesRequest) (*userv1.ListLoginRiskBlockedCountriesResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserHostClient 抽象 gateway 对 user-service host domain 身份查询能力的依赖。
|
// UserHostClient 抽象 gateway 对 user-service host domain 身份查询能力的依赖。
|
||||||
@ -284,6 +285,10 @@ func (c *grpcUserCountryQueryClient) ListRegistrationCountries(ctx context.Conte
|
|||||||
return c.client.ListRegistrationCountries(ctx, req)
|
return c.client.ListRegistrationCountries(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *grpcUserCountryQueryClient) ListLoginRiskBlockedCountries(ctx context.Context, req *userv1.ListLoginRiskBlockedCountriesRequest) (*userv1.ListLoginRiskBlockedCountriesResponse, error) {
|
||||||
|
return c.client.ListLoginRiskBlockedCountries(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
func (c *grpcUserHostClient) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileRequest) (*userv1.GetHostProfileResponse, error) {
|
||||||
return c.client.GetHostProfile(ctx, req)
|
return c.client.GetHostProfile(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,21 +37,22 @@ type AuthHandlers struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppHandlers struct {
|
type AppHandlers struct {
|
||||||
ListRegistrationCountries http.HandlerFunc
|
ListRegistrationCountries http.HandlerFunc
|
||||||
GetAppBootstrap http.HandlerFunc
|
ListLoginRiskBlockedCountries http.HandlerFunc
|
||||||
ListH5Links http.HandlerFunc
|
GetAppBootstrap http.HandlerFunc
|
||||||
ListAppBanners http.HandlerFunc
|
ListH5Links http.HandlerFunc
|
||||||
GetAppVersion http.HandlerFunc
|
ListAppBanners http.HandlerFunc
|
||||||
GetResourceGroup http.HandlerFunc
|
GetAppVersion http.HandlerFunc
|
||||||
ListGifts http.HandlerFunc
|
GetResourceGroup http.HandlerFunc
|
||||||
ListEmojiPacks http.HandlerFunc
|
ListGifts http.HandlerFunc
|
||||||
IssueTencentIMUserSig http.HandlerFunc
|
ListEmojiPacks http.HandlerFunc
|
||||||
IssueTencentRTCToken http.HandlerFunc
|
IssueTencentIMUserSig http.HandlerFunc
|
||||||
HandleTencentIMCallback http.HandlerFunc
|
IssueTencentRTCToken http.HandlerFunc
|
||||||
HandleTencentRTCCallback http.HandlerFunc
|
HandleTencentIMCallback http.HandlerFunc
|
||||||
UploadFile http.HandlerFunc
|
HandleTencentRTCCallback http.HandlerFunc
|
||||||
UploadAvatar http.HandlerFunc
|
UploadFile http.HandlerFunc
|
||||||
HandlePushToken http.HandlerFunc
|
UploadAvatar http.HandlerFunc
|
||||||
|
HandlePushToken http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserHandlers struct {
|
type UserHandlers struct {
|
||||||
@ -223,6 +224,7 @@ func (r routes) registerAuthRoutes() {
|
|||||||
func (r routes) registerAppRoutes() {
|
func (r routes) registerAppRoutes() {
|
||||||
h := r.config.App
|
h := r.config.App
|
||||||
r.public("/countries", "", h.ListRegistrationCountries)
|
r.public("/countries", "", h.ListRegistrationCountries)
|
||||||
|
r.public("/login-risk/blocked-countries", http.MethodGet, h.ListLoginRiskBlockedCountries)
|
||||||
r.public("/app/bootstrap", "", h.GetAppBootstrap)
|
r.public("/app/bootstrap", "", h.GetAppBootstrap)
|
||||||
r.public("/app/h5-links", "", h.ListH5Links)
|
r.public("/app/h5-links", "", h.ListH5Links)
|
||||||
r.public("/app/banners", "", h.ListAppBanners)
|
r.public("/app/banners", "", h.ListAppBanners)
|
||||||
|
|||||||
@ -16,6 +16,12 @@ import (
|
|||||||
|
|
||||||
const resourceTypeEmojiPack = "emoji_pack"
|
const resourceTypeEmojiPack = "emoji_pack"
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultEmojiPackCategory = "默认"
|
||||||
|
emojiPackPricingFree = "free"
|
||||||
|
emojiPackPricingPaid = "paid"
|
||||||
|
)
|
||||||
|
|
||||||
type resourceData struct {
|
type resourceData struct {
|
||||||
ResourceID int64 `json:"resource_id"`
|
ResourceID int64 `json:"resource_id"`
|
||||||
ResourceCode string `json:"resource_code"`
|
ResourceCode string `json:"resource_code"`
|
||||||
@ -86,6 +92,8 @@ type emojiPackData struct {
|
|||||||
ResourceID int64 `json:"resource_id"`
|
ResourceID int64 `json:"resource_id"`
|
||||||
ResourceCode string `json:"resource_code"`
|
ResourceCode string `json:"resource_code"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricing_type"`
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
AnimationURL string `json:"animation_url"`
|
AnimationURL string `json:"animation_url"`
|
||||||
RegionIDs []int64 `json:"region_ids"`
|
RegionIDs []int64 `json:"region_ids"`
|
||||||
@ -94,6 +102,12 @@ type emojiPackData struct {
|
|||||||
UpdatedAtMS int64 `json:"updated_at_ms"`
|
UpdatedAtMS int64 `json:"updated_at_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type emojiPackMetadata struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricing_type"`
|
||||||
|
RegionIDs []int64 `json:"region_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
type userResourceData struct {
|
type userResourceData struct {
|
||||||
EntitlementID string `json:"entitlement_id"`
|
EntitlementID string `json:"entitlement_id"`
|
||||||
ResourceID int64 `json:"resource_id"`
|
ResourceID int64 `json:"resource_id"`
|
||||||
@ -414,30 +428,55 @@ func emojiPackFromProto(item *walletv1.Resource) emojiPackData {
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return emojiPackData{}
|
return emojiPackData{}
|
||||||
}
|
}
|
||||||
regionIDs := emojiRegionIDsFromMetadata(item.GetMetadataJson())
|
metadata := emojiPackMetadataFromJSON(item.GetMetadataJson())
|
||||||
return emojiPackData{
|
return emojiPackData{
|
||||||
ResourceID: item.GetResourceId(),
|
ResourceID: item.GetResourceId(),
|
||||||
ResourceCode: item.GetResourceCode(),
|
ResourceCode: item.GetResourceCode(),
|
||||||
Name: item.GetName(),
|
Name: item.GetName(),
|
||||||
|
Category: metadata.Category,
|
||||||
|
PricingType: metadata.PricingType,
|
||||||
CoverURL: firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()),
|
CoverURL: firstNonEmpty(item.GetPreviewUrl(), item.GetAssetUrl()),
|
||||||
AnimationURL: item.GetAnimationUrl(),
|
AnimationURL: item.GetAnimationUrl(),
|
||||||
RegionIDs: regionIDs,
|
RegionIDs: metadata.RegionIDs,
|
||||||
IsGlobal: len(regionIDs) == 0,
|
IsGlobal: len(metadata.RegionIDs) == 0,
|
||||||
SortOrder: item.GetSortOrder(),
|
SortOrder: item.GetSortOrder(),
|
||||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func emojiRegionIDsFromMetadata(metadataJSON string) []int64 {
|
func emojiPackMetadataFromJSON(metadataJSON string) emojiPackMetadata {
|
||||||
var payload struct {
|
payload := emojiPackMetadata{
|
||||||
RegionIDs []int64 `json:"region_ids"`
|
Category: defaultEmojiPackCategory,
|
||||||
|
PricingType: emojiPackPricingFree,
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
|
if err := json.Unmarshal([]byte(metadataJSON), &payload); err != nil {
|
||||||
return nil
|
return payload
|
||||||
}
|
}
|
||||||
out := make([]int64, 0, len(payload.RegionIDs))
|
payload.Category = normalizeEmojiPackCategory(payload.Category)
|
||||||
seen := make(map[int64]struct{}, len(payload.RegionIDs))
|
payload.PricingType = normalizeEmojiPackPricingType(payload.PricingType)
|
||||||
for _, regionID := range payload.RegionIDs {
|
payload.RegionIDs = normalizeEmojiPackRegionIDs(payload.RegionIDs)
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmojiPackCategory(value string) string {
|
||||||
|
category := strings.TrimSpace(value)
|
||||||
|
if category == "" {
|
||||||
|
return defaultEmojiPackCategory
|
||||||
|
}
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmojiPackPricingType(value string) string {
|
||||||
|
if strings.ToLower(strings.TrimSpace(value)) == emojiPackPricingPaid {
|
||||||
|
return emojiPackPricingPaid
|
||||||
|
}
|
||||||
|
return emojiPackPricingFree
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEmojiPackRegionIDs(regionIDs []int64) []int64 {
|
||||||
|
out := make([]int64, 0, len(regionIDs))
|
||||||
|
seen := make(map[int64]struct{}, len(regionIDs))
|
||||||
|
for _, regionID := range regionIDs {
|
||||||
if regionID <= 0 {
|
if regionID <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -302,9 +302,12 @@ type fakeUserDeviceClient struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeUserCountryQueryClient struct {
|
type fakeUserCountryQueryClient struct {
|
||||||
last *userv1.ListRegistrationCountriesRequest
|
last *userv1.ListRegistrationCountriesRequest
|
||||||
resp *userv1.ListRegistrationCountriesResponse
|
resp *userv1.ListRegistrationCountriesResponse
|
||||||
err error
|
err error
|
||||||
|
lastBlocked *userv1.ListLoginRiskBlockedCountriesRequest
|
||||||
|
blockedResp *userv1.ListLoginRiskBlockedCountriesResponse
|
||||||
|
blockedErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeUserHostClient struct {
|
type fakeUserHostClient struct {
|
||||||
@ -734,6 +737,18 @@ func (f *fakeUserCountryQueryClient) ListRegistrationCountries(_ context.Context
|
|||||||
return &userv1.ListRegistrationCountriesResponse{}, nil
|
return &userv1.ListRegistrationCountriesResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeUserCountryQueryClient) ListLoginRiskBlockedCountries(_ context.Context, req *userv1.ListLoginRiskBlockedCountriesRequest) (*userv1.ListLoginRiskBlockedCountriesResponse, error) {
|
||||||
|
f.lastBlocked = req
|
||||||
|
if f.blockedErr != nil {
|
||||||
|
return nil, f.blockedErr
|
||||||
|
}
|
||||||
|
if f.blockedResp != nil {
|
||||||
|
return f.blockedResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userv1.ListLoginRiskBlockedCountriesResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeMessageInboxClient) ListMessageTabs(_ context.Context, req *activityv1.ListMessageTabsRequest) (*activityv1.ListMessageTabsResponse, error) {
|
func (f *fakeMessageInboxClient) ListMessageTabs(_ context.Context, req *activityv1.ListMessageTabsRequest) (*activityv1.ListMessageTabsResponse, error) {
|
||||||
f.lastTabs = req
|
f.lastTabs = req
|
||||||
if f.err != nil {
|
if f.err != nil {
|
||||||
@ -2057,6 +2072,46 @@ func TestListRegistrationCountriesIsPublicAndReturnsCountryMetadata(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListLoginRiskBlockedCountriesIsPublic(t *testing.T) {
|
||||||
|
countryClient := &fakeUserCountryQueryClient{blockedResp: &userv1.ListLoginRiskBlockedCountriesResponse{
|
||||||
|
Countries: []*userv1.LoginRiskBlockedCountry{
|
||||||
|
{CountryCode: "CN", Keyword: "中国", UpdatedAtMs: 1700000000000},
|
||||||
|
},
|
||||||
|
UpdatedAtMs: 1700000000000,
|
||||||
|
}}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
|
handler.SetUserCountryQueryClient(countryClient)
|
||||||
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/login-risk/blocked-countries", nil)
|
||||||
|
request.Header.Set("X-Request-ID", "req-risk-blocks")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if countryClient.lastBlocked == nil || countryClient.lastBlocked.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-risk-blocks") {
|
||||||
|
t.Fatalf("login risk block request metadata mismatch: %+v", countryClient.lastBlocked)
|
||||||
|
}
|
||||||
|
var response httpkit.ResponseEnvelope
|
||||||
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
|
t.Fatalf("decode response failed: %v", err)
|
||||||
|
}
|
||||||
|
data, ok := response.Data.(map[string]any)
|
||||||
|
if response.Code != httpkit.CodeOK || !ok {
|
||||||
|
t.Fatalf("unexpected envelope: %+v", response)
|
||||||
|
}
|
||||||
|
countries, ok := data["countries"].([]any)
|
||||||
|
if !ok || len(countries) != 1 || data["updated_at_ms"].(float64) != 1700000000000 {
|
||||||
|
t.Fatalf("blocked countries response shape mismatch: %+v", data)
|
||||||
|
}
|
||||||
|
country, ok := countries[0].(map[string]any)
|
||||||
|
if !ok || country["country_code"] != "CN" || country["keyword"] != "中国" {
|
||||||
|
t.Fatalf("blocked country metadata mismatch: %+v", country)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListH5LinksReturnsAdminAppConfig(t *testing.T) {
|
func TestListH5LinksReturnsAdminAppConfig(t *testing.T) {
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||||
handler.SetAppConfigReader(appconfig.StaticReader{Links: []appconfig.H5Link{
|
handler.SetAppConfigReader(appconfig.StaticReader{Links: []appconfig.H5Link{
|
||||||
@ -2947,7 +3002,7 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
|||||||
Status: "active",
|
Status: "active",
|
||||||
PreviewUrl: "https://cdn.example/emoji/global.png",
|
PreviewUrl: "https://cdn.example/emoji/global.png",
|
||||||
AnimationUrl: "https://cdn.example/emoji/global.svga",
|
AnimationUrl: "https://cdn.example/emoji/global.svga",
|
||||||
MetadataJson: `{"region_scope":"all"}`,
|
MetadataJson: `{"category":"默认","pricing_type":"free","region_scope":"all"}`,
|
||||||
SortOrder: 10,
|
SortOrder: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2958,7 +3013,7 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
|||||||
Status: "active",
|
Status: "active",
|
||||||
AssetUrl: "https://cdn.example/emoji/saudi.png",
|
AssetUrl: "https://cdn.example/emoji/saudi.png",
|
||||||
AnimationUrl: "https://cdn.example/emoji/saudi.pag",
|
AnimationUrl: "https://cdn.example/emoji/saudi.pag",
|
||||||
MetadataJson: `{"region_ids":[1001]}`,
|
MetadataJson: `{"category":"热门","pricing_type":"paid","region_ids":[1001]}`,
|
||||||
SortOrder: 20,
|
SortOrder: 20,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2991,6 +3046,8 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
|||||||
Data struct {
|
Data struct {
|
||||||
Items []struct {
|
Items []struct {
|
||||||
ResourceID int64 `json:"resource_id"`
|
ResourceID int64 `json:"resource_id"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
PricingType string `json:"pricing_type"`
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
AnimationURL string `json:"animation_url"`
|
AnimationURL string `json:"animation_url"`
|
||||||
RegionIDs []int64 `json:"region_ids"`
|
RegionIDs []int64 `json:"region_ids"`
|
||||||
@ -3008,10 +3065,10 @@ func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
|
|||||||
if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 2 || len(envelope.Data.Items) != 2 {
|
if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 2 || len(envelope.Data.Items) != 2 {
|
||||||
t.Fatalf("emoji pack envelope mismatch: %+v", envelope)
|
t.Fatalf("emoji pack envelope mismatch: %+v", envelope)
|
||||||
}
|
}
|
||||||
if !envelope.Data.Items[0].IsGlobal || envelope.Data.Items[0].CoverURL != "https://cdn.example/emoji/global.png" {
|
if !envelope.Data.Items[0].IsGlobal || envelope.Data.Items[0].CoverURL != "https://cdn.example/emoji/global.png" || envelope.Data.Items[0].Category != "默认" || envelope.Data.Items[0].PricingType != "free" {
|
||||||
t.Fatalf("global emoji pack mismatch: %+v", envelope.Data.Items[0])
|
t.Fatalf("global emoji pack mismatch: %+v", envelope.Data.Items[0])
|
||||||
}
|
}
|
||||||
if envelope.Data.Items[1].ResourceID != 7002 || envelope.Data.Items[1].AnimationURL != "https://cdn.example/emoji/saudi.pag" || len(envelope.Data.Items[1].RegionIDs) != 1 || envelope.Data.Items[1].RegionIDs[0] != 1001 {
|
if envelope.Data.Items[1].ResourceID != 7002 || envelope.Data.Items[1].AnimationURL != "https://cdn.example/emoji/saudi.pag" || envelope.Data.Items[1].Category != "热门" || envelope.Data.Items[1].PricingType != "paid" || len(envelope.Data.Items[1].RegionIDs) != 1 || envelope.Data.Items[1].RegionIDs[0] != 1001 {
|
||||||
t.Fatalf("regional emoji pack mismatch: %+v", envelope.Data.Items[1])
|
t.Fatalf("regional emoji pack mismatch: %+v", envelope.Data.Items[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,21 +118,22 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
|
|||||||
Logout: h.logout,
|
Logout: h.logout,
|
||||||
},
|
},
|
||||||
App: httproutes.AppHandlers{
|
App: httproutes.AppHandlers{
|
||||||
ListRegistrationCountries: userAPI.ListRegistrationCountries,
|
ListRegistrationCountries: userAPI.ListRegistrationCountries,
|
||||||
GetAppBootstrap: appAPI.GetAppBootstrap,
|
ListLoginRiskBlockedCountries: userAPI.ListLoginRiskBlockedCountries,
|
||||||
ListH5Links: appAPI.ListH5Links,
|
GetAppBootstrap: appAPI.GetAppBootstrap,
|
||||||
ListAppBanners: appAPI.ListAppBanners,
|
ListH5Links: appAPI.ListH5Links,
|
||||||
GetAppVersion: appAPI.GetAppVersion,
|
ListAppBanners: appAPI.ListAppBanners,
|
||||||
GetResourceGroup: resourceAPI.GetResourceGroup,
|
GetAppVersion: appAPI.GetAppVersion,
|
||||||
ListGifts: resourceAPI.ListGifts,
|
GetResourceGroup: resourceAPI.GetResourceGroup,
|
||||||
ListEmojiPacks: resourceAPI.ListEmojiPacks,
|
ListGifts: resourceAPI.ListGifts,
|
||||||
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
ListEmojiPacks: resourceAPI.ListEmojiPacks,
|
||||||
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
IssueTencentIMUserSig: h.issueTencentIMUserSig,
|
||||||
HandleTencentIMCallback: callbackAPI.HandleTencentIMCallback,
|
IssueTencentRTCToken: roomAPI.IssueTencentRTCToken,
|
||||||
HandleTencentRTCCallback: callbackAPI.HandleTencentRTCCallback,
|
HandleTencentIMCallback: callbackAPI.HandleTencentIMCallback,
|
||||||
UploadFile: appAPI.UploadFile,
|
HandleTencentRTCCallback: callbackAPI.HandleTencentRTCCallback,
|
||||||
UploadAvatar: appAPI.UploadAvatar,
|
UploadFile: appAPI.UploadFile,
|
||||||
HandlePushToken: appAPI.HandlePushToken,
|
UploadAvatar: appAPI.UploadAvatar,
|
||||||
|
HandlePushToken: appAPI.HandlePushToken,
|
||||||
},
|
},
|
||||||
User: userHandlers,
|
User: userHandlers,
|
||||||
Manager: managerAPI.ManagerHandlers(),
|
Manager: managerAPI.ManagerHandlers(),
|
||||||
|
|||||||
@ -65,3 +65,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
|
|||||||
func (h *Handler) ListRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) ListRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
h.listRegistrationCountries(writer, request)
|
h.listRegistrationCountries(writer, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ListLoginRiskBlockedCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
h.listLoginRiskBlockedCountries(writer, request)
|
||||||
|
}
|
||||||
|
|||||||
@ -84,6 +84,14 @@ type countryData struct {
|
|||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loginRiskBlockedCountryData 是 App 登录后异步地区风控只读词表。
|
||||||
|
// keyword 保留后台录入词,country_code 为空时表示该词只供客户端本地匹配。
|
||||||
|
type loginRiskBlockedCountryData struct {
|
||||||
|
CountryCode string `json:"country_code,omitempty"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
UpdatedAtMs int64 `json:"updated_at_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
// listRegistrationCountries 返回 App 注册页可选国家。
|
// listRegistrationCountries 返回 App 注册页可选国家。
|
||||||
// 该接口公开读,不要求 access token;user-service 负责只返回 active 且 enabled 的国家。
|
// 该接口公开读,不要求 access token;user-service 负责只返回 active 且 enabled 的国家。
|
||||||
func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
@ -107,6 +115,33 @@ func (h *Handler) listRegistrationCountries(writer http.ResponseWriter, request
|
|||||||
httpkit.WriteOK(writer, request, map[string]any{"countries": countries})
|
httpkit.WriteOK(writer, request, map[string]any{"countries": countries})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listLoginRiskBlockedCountries 返回 App 可异步拉取的地区屏蔽词表。
|
||||||
|
// 登录最终处置仍由 user-service 风控 worker 决定,客户端只做补充风险探测和上报。
|
||||||
|
func (h *Handler) listLoginRiskBlockedCountries(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if h.userCountryClient == nil {
|
||||||
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.userCountryClient.ListLoginRiskBlockedCountries(request.Context(), &userv1.ListLoginRiskBlockedCountriesRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
countries := make([]loginRiskBlockedCountryData, 0, len(resp.GetCountries()))
|
||||||
|
for _, country := range resp.GetCountries() {
|
||||||
|
countries = append(countries, loginRiskBlockedCountryData{
|
||||||
|
CountryCode: country.GetCountryCode(),
|
||||||
|
Keyword: country.GetKeyword(),
|
||||||
|
UpdatedAtMs: country.GetUpdatedAtMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
httpkit.WriteOK(writer, request, map[string]any{"countries": countries, "updated_at_ms": resp.GetUpdatedAtMs()})
|
||||||
|
}
|
||||||
|
|
||||||
// getMyIdentity 返回当前登录用户的短号状态。
|
// getMyIdentity 返回当前登录用户的短号状态。
|
||||||
func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) getMyIdentity(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.userIdentityClient == nil {
|
if h.userIdentityClient == nil {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/notice-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/notice-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,12 @@ room_database: "hyapp_room"
|
|||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
enabled: false
|
enabled: true
|
||||||
sdk_app_id: 0
|
sdk_app_id: 20036101
|
||||||
secret_key: ""
|
secret_key: "dcdf53135f27e7cf8541c4ae9798fb0cb2f0e4d6c5c20022dbaea053f35cbb8c"
|
||||||
admin_identifier: "administrator"
|
admin_identifier: "900100100"
|
||||||
admin_user_sig_ttl: 24h
|
admin_user_sig_ttl: 24h
|
||||||
endpoint: "console.tim.qq.com"
|
endpoint: "adminapisgp.im.qcloud.com"
|
||||||
request_timeout: 5s
|
request_timeout: 5s
|
||||||
|
|
||||||
wallet_notice_worker:
|
wallet_notice_worker:
|
||||||
@ -28,7 +28,7 @@ wallet_notice_worker:
|
|||||||
max_backoff: 5m
|
max_backoff: 5m
|
||||||
|
|
||||||
room_notice_worker:
|
room_notice_worker:
|
||||||
enabled: false
|
enabled: true
|
||||||
poll_interval: 1s
|
poll_interval: 1s
|
||||||
batch_size: 100
|
batch_size: 100
|
||||||
lock_ttl: 30s
|
lock_ttl: 30s
|
||||||
|
|||||||
@ -9,12 +9,12 @@ room_database: "hyapp_room"
|
|||||||
mysql_auto_migrate: false
|
mysql_auto_migrate: false
|
||||||
|
|
||||||
tencent_im:
|
tencent_im:
|
||||||
enabled: false
|
enabled: true
|
||||||
sdk_app_id: 0
|
sdk_app_id: 20036101
|
||||||
secret_key: ""
|
secret_key: "dcdf53135f27e7cf8541c4ae9798fb0cb2f0e4d6c5c20022dbaea053f35cbb8c"
|
||||||
admin_identifier: "administrator"
|
admin_identifier: "900100100"
|
||||||
admin_user_sig_ttl: 24h
|
admin_user_sig_ttl: 24h
|
||||||
endpoint: "console.tim.qq.com"
|
endpoint: "adminapisgp.im.qcloud.com"
|
||||||
request_timeout: 5s
|
request_timeout: 5s
|
||||||
|
|
||||||
wallet_notice_worker:
|
wallet_notice_worker:
|
||||||
@ -28,7 +28,7 @@ wallet_notice_worker:
|
|||||||
max_backoff: 5m
|
max_backoff: 5m
|
||||||
|
|
||||||
room_notice_worker:
|
room_notice_worker:
|
||||||
enabled: false
|
enabled: true
|
||||||
poll_interval: 1s
|
poll_interval: 1s
|
||||||
batch_size: 100
|
batch_size: 100
|
||||||
lock_ttl: 30s
|
lock_ttl: 30s
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/room-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/room-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
@ -29,11 +29,11 @@ tencent_im:
|
|||||||
group_type: "ChatRoom"
|
group_type: "ChatRoom"
|
||||||
request_timeout: "5s"
|
request_timeout: "5s"
|
||||||
tencent_rtc:
|
tencent_rtc:
|
||||||
# Docker 本地默认关闭真实 TRTC 管理 API;开启后封禁会同步调用 RemoveUserByStrRoomId。
|
# Docker 本地联调真实 TRTC 管理 API;关闭房间会同步调用 RemoveUserByStrRoomId。
|
||||||
enabled: false
|
enabled: true
|
||||||
sdk_app_id: 0
|
sdk_app_id: 20036101
|
||||||
secret_id: ""
|
secret_id: "IKIDMchhZEfrsiNo472DAtTpzzmLjttkOnyu"
|
||||||
secret_key: ""
|
secret_key: "nMkbLsGRO6ZqulSyJQJ0UjjU0KSKxOgl"
|
||||||
region: "ap-guangzhou"
|
region: "ap-guangzhou"
|
||||||
endpoint: "trtc.tencentcloudapi.com"
|
endpoint: "trtc.tencentcloudapi.com"
|
||||||
request_timeout: "5s"
|
request_timeout: "5s"
|
||||||
|
|||||||
@ -32,11 +32,11 @@ tencent_im:
|
|||||||
# outbox worker 建群和发送房间系统消息的单次 REST 超时。
|
# outbox worker 建群和发送房间系统消息的单次 REST 超时。
|
||||||
request_timeout: "5s"
|
request_timeout: "5s"
|
||||||
tencent_rtc:
|
tencent_rtc:
|
||||||
# 本地默认关闭真实 TRTC 管理 API;开启后封禁会同步调用 RemoveUserByStrRoomId。
|
# 本地联调真实 TRTC 管理 API;关闭房间会同步调用 RemoveUserByStrRoomId。
|
||||||
enabled: false
|
enabled: true
|
||||||
sdk_app_id: 0
|
sdk_app_id: 20036101
|
||||||
secret_id: ""
|
secret_id: "IKIDMchhZEfrsiNo472DAtTpzzmLjttkOnyu"
|
||||||
secret_key: ""
|
secret_key: "nMkbLsGRO6ZqulSyJQJ0UjjU0KSKxOgl"
|
||||||
region: "ap-guangzhou"
|
region: "ap-guangzhou"
|
||||||
endpoint: "trtc.tencentcloudapi.com"
|
endpoint: "trtc.tencentcloudapi.com"
|
||||||
request_timeout: "5s"
|
request_timeout: "5s"
|
||||||
|
|||||||
@ -15,6 +15,10 @@ CREATE TABLE IF NOT EXISTS rooms (
|
|||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
close_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
closed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||||||
created_at_ms BIGINT NOT NULL,
|
created_at_ms BIGINT NOT NULL,
|
||||||
updated_at_ms BIGINT NOT NULL,
|
updated_at_ms BIGINT NOT NULL,
|
||||||
@ -35,6 +39,10 @@ CREATE TABLE IF NOT EXISTS room_list_entries (
|
|||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
close_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
closed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
heat BIGINT NOT NULL DEFAULT 0,
|
heat BIGINT NOT NULL DEFAULT 0,
|
||||||
online_count INT NOT NULL DEFAULT 0,
|
online_count INT NOT NULL DEFAULT 0,
|
||||||
seat_count INT NOT NULL DEFAULT 0,
|
seat_count INT NOT NULL DEFAULT 0,
|
||||||
|
|||||||
@ -130,6 +130,15 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
|||||||
base.ActorUserID = body.GetUserId()
|
base.ActorUserID = body.GetUserId()
|
||||||
base.TargetUserID = body.GetUserId()
|
base.TargetUserID = body.GetUserId()
|
||||||
return base, true, nil
|
return base, true, nil
|
||||||
|
case "RoomClosed":
|
||||||
|
// 关房事件提示仍在群内的客户端刷新房间终态;私有通知由 RoomUserKicked 事实另行投递。
|
||||||
|
var body roomeventsv1.RoomClosed
|
||||||
|
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||||
|
return tencentim.RoomEvent{}, false, err
|
||||||
|
}
|
||||||
|
base.ActorUserID = body.GetActorUserId()
|
||||||
|
base.Attributes = map[string]string{"reason": body.GetReason()}
|
||||||
|
return base, true, nil
|
||||||
case "RoomMicChanged":
|
case "RoomMicChanged":
|
||||||
// 麦位变更把 from/to 合并成一个客户端事件,避免换麦出现中间空位状态。
|
// 麦位变更把 from/to 合并成一个客户端事件,避免换麦出现中间空位状态。
|
||||||
var body roomeventsv1.RoomMicChanged
|
var body roomeventsv1.RoomMicChanged
|
||||||
@ -260,6 +269,8 @@ func eventTypeForClient(eventType string) string {
|
|||||||
return "room_user_joined"
|
return "room_user_joined"
|
||||||
case "RoomUserLeft":
|
case "RoomUserLeft":
|
||||||
return "room_user_left"
|
return "room_user_left"
|
||||||
|
case "RoomClosed":
|
||||||
|
return "room_closed"
|
||||||
case "RoomMicChanged":
|
case "RoomMicChanged":
|
||||||
return "room_mic_changed"
|
return "room_mic_changed"
|
||||||
case "RoomMicSeatLocked":
|
case "RoomMicSeatLocked":
|
||||||
|
|||||||
@ -2,18 +2,28 @@ package service_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||||
roomv1 "hyapp.local/api/proto/room/v1"
|
roomv1 "hyapp.local/api/proto/room/v1"
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/services/room-service/internal/integration"
|
"hyapp/services/room-service/internal/integration"
|
||||||
|
roomoutbox "hyapp/services/room-service/internal/room/outbox"
|
||||||
roomservice "hyapp/services/room-service/internal/room/service"
|
roomservice "hyapp/services/room-service/internal/room/service"
|
||||||
"hyapp/services/room-service/internal/router"
|
"hyapp/services/room-service/internal/router"
|
||||||
"hyapp/services/room-service/internal/testutil/mysqltest"
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeRTCUserRemover struct {
|
type fakeRTCUserRemover struct {
|
||||||
|
calls []rtcRemoveCall
|
||||||
|
roomID string
|
||||||
|
userID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type rtcRemoveCall struct {
|
||||||
roomID string
|
roomID string
|
||||||
userID int64
|
userID int64
|
||||||
}
|
}
|
||||||
@ -21,6 +31,7 @@ type fakeRTCUserRemover struct {
|
|||||||
func (r *fakeRTCUserRemover) RemoveUserByStrRoomID(_ context.Context, roomID string, userID int64) error {
|
func (r *fakeRTCUserRemover) RemoveUserByStrRoomID(_ context.Context, roomID string, userID int64) error {
|
||||||
r.roomID = roomID
|
r.roomID = roomID
|
||||||
r.userID = userID
|
r.userID = userID
|
||||||
|
r.calls = append(r.calls, rtcRemoveCall{roomID: roomID, userID: userID})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +100,95 @@ func TestKickUserRemovesPresenceBanAndRTCConnection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCloseRoomByAdminEvictsCurrentUsersThroughOutboxAndRTC(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
rtc := &fakeRTCUserRemover{}
|
||||||
|
svc := roomservice.New(roomservice.Config{
|
||||||
|
NodeID: "node-close-test",
|
||||||
|
LeaseTTL: 10 * time.Second,
|
||||||
|
RankLimit: 20,
|
||||||
|
SnapshotEveryN: 1,
|
||||||
|
RTCUserRemover: rtc,
|
||||||
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||||
|
|
||||||
|
roomID := "room-close-flow"
|
||||||
|
ownerID := int64(8101)
|
||||||
|
viewerID := int64(8201)
|
||||||
|
adminID := int64(1)
|
||||||
|
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
||||||
|
SeatCount: 10,
|
||||||
|
Mode: "voice",
|
||||||
|
VisibleRegionId: 8101,
|
||||||
|
RoomName: "Close Flow",
|
||||||
|
RoomShortId: "close-flow",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create close room fixture failed: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("join close target failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := svc.CloseRoom(ctx, &roomv1.CloseRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{
|
||||||
|
RequestId: "req-close-admin",
|
||||||
|
CommandId: "cmd-close-admin",
|
||||||
|
ActorUserId: adminID,
|
||||||
|
RoomId: roomID,
|
||||||
|
AppCode: appcode.Default,
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
Reason: "admin_closed",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin close room failed: %v", err)
|
||||||
|
}
|
||||||
|
if resp.GetRoom().GetStatus() != "closed" || len(resp.GetRoom().GetOnlineUsers()) != 0 {
|
||||||
|
t.Fatalf("admin close response must close room and clear users: %+v", resp.GetRoom())
|
||||||
|
}
|
||||||
|
if !hasRTCRemoveCall(rtc.calls, roomID, ownerID) || !hasRTCRemoveCall(rtc.calls, roomID, viewerID) {
|
||||||
|
t.Fatalf("admin close must remove all current RTC users: %+v", rtc.calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := repository.ListPendingOutbox(ctx, 20)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list pending outbox failed: %v", err)
|
||||||
|
}
|
||||||
|
if !hasOutboxEvent(records, "RoomClosed", 0) ||
|
||||||
|
!hasOutboxEvent(records, "RoomUserKicked", ownerID) ||
|
||||||
|
!hasOutboxEvent(records, "RoomUserKicked", viewerID) {
|
||||||
|
t.Fatalf("admin close must write room closed and per-user kick events: %+v", outboxSummary(records))
|
||||||
|
}
|
||||||
|
|
||||||
|
reopenResp, err := svc.CloseRoom(ctx, &roomv1.CloseRoomRequest{
|
||||||
|
Meta: &roomv1.RequestMeta{
|
||||||
|
RequestId: "req-reopen-admin",
|
||||||
|
CommandId: "cmd-reopen-admin",
|
||||||
|
ActorUserId: adminID,
|
||||||
|
RoomId: roomID,
|
||||||
|
AppCode: appcode.Default,
|
||||||
|
SentAtMs: time.Now().UnixMilli(),
|
||||||
|
},
|
||||||
|
Reason: "admin_reopen",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin reopen room failed: %v", err)
|
||||||
|
}
|
||||||
|
if reopenResp.GetRoom().GetStatus() != "active" {
|
||||||
|
t.Fatalf("admin reopen response must make room active: %+v", reopenResp.GetRoom())
|
||||||
|
}
|
||||||
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||||
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
||||||
|
Role: "audience",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("viewer must join after admin reopen: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func findRoomUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
|
func findRoomUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
|
||||||
for _, user := range snapshot.GetOnlineUsers() {
|
for _, user := range snapshot.GetOnlineUsers() {
|
||||||
if user.GetUserId() == userID {
|
if user.GetUserId() == userID {
|
||||||
@ -106,3 +206,39 @@ func containsInt64(values []int64, target int64) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasRTCRemoveCall(calls []rtcRemoveCall, roomID string, userID int64) bool {
|
||||||
|
for _, call := range calls {
|
||||||
|
if call.roomID == roomID && call.userID == userID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasOutboxEvent(records []roomoutbox.Record, eventType string, targetUserID int64) bool {
|
||||||
|
for _, record := range records {
|
||||||
|
if record.EventType != eventType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if targetUserID == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var body roomeventsv1.RoomUserKicked
|
||||||
|
if record.Envelope == nil || proto.Unmarshal(record.Envelope.GetBody(), &body) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if body.GetTargetUserId() == targetUserID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func outboxSummary(records []roomoutbox.Record) []string {
|
||||||
|
summary := make([]string, 0, len(records))
|
||||||
|
for _, record := range records {
|
||||||
|
summary = append(summary, fmt.Sprintf("%s:%s", record.EventType, record.EventID))
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -49,7 +50,11 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
if cmd.Reason == "" {
|
if cmd.Reason == "" {
|
||||||
cmd.Reason = "closed_by_owner"
|
cmd.Reason = "closed_by_owner"
|
||||||
}
|
}
|
||||||
|
if isAdminReopenReason(cmd.Reason) {
|
||||||
|
return s.reopenRoom(ctx, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
rtcKickTargets := make([]int64, 0)
|
||||||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||||||
if current == nil {
|
if current == nil {
|
||||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||||||
@ -62,8 +67,15 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
|
// creating 等非开放状态不允许用 CloseRoom 直接跳过创建收尾语义。
|
||||||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not active")
|
||||||
}
|
}
|
||||||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
if !isAdminCloseReason(cmd.Reason) {
|
||||||
return mutationResult{}, nil, err
|
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kickTargets := closeRoomKickTargets(current)
|
||||||
|
if len(kickTargets) > 0 {
|
||||||
|
rtcKickTargets = append(rtcKickTargets[:0], kickTargets...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type occupiedMicSeat struct {
|
type occupiedMicSeat struct {
|
||||||
@ -91,6 +103,7 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
}
|
}
|
||||||
current.Version++
|
current.Version++
|
||||||
|
|
||||||
|
records := make([]outbox.Record, 0, len(occupiedSeats)+len(kickTargets)+1)
|
||||||
closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version, now, &roomeventsv1.RoomClosed{
|
closedEvent, err := outbox.Build(current.RoomID, "RoomClosed", current.Version, now, &roomeventsv1.RoomClosed{
|
||||||
ActorUserId: cmd.ActorUserID(),
|
ActorUserId: cmd.ActorUserID(),
|
||||||
Reason: cmd.Reason,
|
Reason: cmd.Reason,
|
||||||
@ -98,7 +111,20 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return mutationResult{}, nil, err
|
return mutationResult{}, nil, err
|
||||||
}
|
}
|
||||||
records := make([]outbox.Record, 0, len(occupiedSeats)+1)
|
records = append(records, closedEvent)
|
||||||
|
|
||||||
|
for _, targetUserID := range kickTargets {
|
||||||
|
// 关房清场复用 RoomUserKicked 事实,notice-service 和腾讯 IM 群成员移除可沿用同一条消费链路。
|
||||||
|
kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
|
||||||
|
ActorUserId: cmd.ActorUserID(),
|
||||||
|
TargetUserId: targetUserID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return mutationResult{}, nil, err
|
||||||
|
}
|
||||||
|
records = append(records, kickEvent)
|
||||||
|
}
|
||||||
|
|
||||||
for _, seat := range occupiedSeats {
|
for _, seat := range occupiedSeats {
|
||||||
// 关房是 Room Cell 统一清场,给每个占麦 session 单独写 down 事件,保证用户时长能闭合。
|
// 关房是 Room Cell 统一清场,给每个占麦 session 单独写 down 事件,保证用户时长能闭合。
|
||||||
micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), seat.userID, seat.seatNo, seat.micSessionID, "room_closed")
|
micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), seat.userID, seat.seatNo, seat.micSessionID, "room_closed")
|
||||||
@ -107,7 +133,6 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
}
|
}
|
||||||
records = append(records, micEvent)
|
records = append(records, micEvent)
|
||||||
}
|
}
|
||||||
records = append(records, closedEvent)
|
|
||||||
|
|
||||||
return mutationResult{
|
return mutationResult{
|
||||||
snapshot: current.ToProto(),
|
snapshot: current.ToProto(),
|
||||||
@ -128,8 +153,78 @@ func (s *Service) CloseRoom(ctx context.Context, req *roomv1.CloseRoomRequest) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response := &roomv1.CloseRoomResponse{
|
||||||
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||||||
|
Room: result.snapshot,
|
||||||
|
}
|
||||||
|
if result.applied && s.rtcUserRemover != nil {
|
||||||
|
for _, targetUserID := range rtcKickTargets {
|
||||||
|
// RTC 是房间外连接态:关房事实已提交后再逐个移除,失败不回滚 Room Cell。
|
||||||
|
_ = s.rtcUserRemover.RemoveUserByStrRoomID(ctx, cmd.RoomID(), targetUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdminCloseReason(reason string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(reason), "admin_closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdminReopenReason(reason string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(reason), "admin_reopen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) reopenRoom(ctx context.Context, cmd command.CloseRoom) (*roomv1.CloseRoomResponse, error) {
|
||||||
|
result, err := s.mutateRoom(ctx, cmd, true, func(_ time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||||||
|
if current == nil {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||||||
|
}
|
||||||
|
if current.Status == state.RoomStatusActive {
|
||||||
|
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||||||
|
}
|
||||||
|
if current.Status != state.RoomStatusClosed {
|
||||||
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "room is not closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
current.Status = state.RoomStatusActive
|
||||||
|
current.Version++
|
||||||
|
return mutationResult{
|
||||||
|
snapshot: current.ToProto(),
|
||||||
|
roomStatus: state.RoomStatusActive,
|
||||||
|
}, nil, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &roomv1.CloseRoomResponse{
|
return &roomv1.CloseRoomResponse{
|
||||||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||||||
Room: result.snapshot,
|
Room: result.snapshot,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeRoomKickTargets(current *state.RoomState) []int64 {
|
||||||
|
if current == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := make(map[int64]struct{}, len(current.OnlineUsers)+len(current.MicSeats))
|
||||||
|
for userID := range current.OnlineUsers {
|
||||||
|
if userID > 0 {
|
||||||
|
seen[userID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, seat := range current.MicSeats {
|
||||||
|
if seat.UserID > 0 {
|
||||||
|
seen[seat.UserID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targets := make([]int64, 0, len(seen))
|
||||||
|
for userID := range seen {
|
||||||
|
targets = append(targets, userID)
|
||||||
|
}
|
||||||
|
sort.Slice(targets, func(left, right int) bool {
|
||||||
|
return targets[left] < targets[right]
|
||||||
|
})
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
|||||||
@ -80,6 +80,10 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
room_password_hash VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
close_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
closed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||||||
created_at_ms BIGINT NOT NULL,
|
created_at_ms BIGINT NOT NULL,
|
||||||
updated_at_ms BIGINT NOT NULL,
|
updated_at_ms BIGINT NOT NULL,
|
||||||
@ -99,6 +103,10 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
mode VARCHAR(64) NOT NULL,
|
mode VARCHAR(64) NOT NULL,
|
||||||
status VARCHAR(32) NOT NULL,
|
status VARCHAR(32) NOT NULL,
|
||||||
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
close_reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
closed_at_ms BIGINT NOT NULL DEFAULT 0,
|
||||||
heat BIGINT NOT NULL DEFAULT 0,
|
heat BIGINT NOT NULL DEFAULT 0,
|
||||||
online_count INT NOT NULL DEFAULT 0,
|
online_count INT NOT NULL DEFAULT 0,
|
||||||
seat_count INT NOT NULL DEFAULT 0,
|
seat_count INT NOT NULL DEFAULT 0,
|
||||||
@ -245,6 +253,9 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
if err := r.ensureRoomPasswordSchema(ctx); err != nil {
|
if err := r.ensureRoomPasswordSchema(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := r.ensureRoomAdminCloseSchema(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return r.ensureOutboxRetrySchema(ctx)
|
return r.ensureOutboxRetrySchema(ctx)
|
||||||
}
|
}
|
||||||
@ -277,6 +288,35 @@ func (r *Repository) ensureRoomListLockSchema(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ensureRoomAdminCloseSchema(ctx context.Context) error {
|
||||||
|
columns := []struct {
|
||||||
|
table string
|
||||||
|
column string
|
||||||
|
statement string
|
||||||
|
}{
|
||||||
|
{"rooms", "close_reason", `ALTER TABLE rooms ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER room_password_hash`},
|
||||||
|
{"rooms", "closed_by_admin_id", `ALTER TABLE rooms ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
|
||||||
|
{"rooms", "closed_by_admin_name", `ALTER TABLE rooms ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
|
||||||
|
{"rooms", "closed_at_ms", `ALTER TABLE rooms ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
|
||||||
|
{"room_list_entries", "close_reason", `ALTER TABLE room_list_entries ADD COLUMN close_reason VARCHAR(512) NOT NULL DEFAULT '' AFTER locked`},
|
||||||
|
{"room_list_entries", "closed_by_admin_id", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER close_reason`},
|
||||||
|
{"room_list_entries", "closed_by_admin_name", `ALTER TABLE room_list_entries ADD COLUMN closed_by_admin_name VARCHAR(128) NOT NULL DEFAULT '' AFTER closed_by_admin_id`},
|
||||||
|
{"room_list_entries", "closed_at_ms", `ALTER TABLE room_list_entries ADD COLUMN closed_at_ms BIGINT NOT NULL DEFAULT 0 AFTER closed_by_admin_name`},
|
||||||
|
}
|
||||||
|
for _, item := range columns {
|
||||||
|
hasColumn, err := r.columnExists(ctx, item.table, item.column)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasColumn {
|
||||||
|
if _, err := r.db.ExecContext(ctx, item.statement); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) ensureOutboxRetrySchema(ctx context.Context) error {
|
func (r *Repository) ensureOutboxRetrySchema(ctx context.Context) error {
|
||||||
hasColumn, err := r.columnExists(ctx, "room_outbox", "next_retry_at_ms")
|
hasColumn, err := r.columnExists(ctx, "room_outbox", "next_retry_at_ms")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/user-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/user-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -622,40 +622,24 @@ CREATE TABLE IF NOT EXISTS host_outbox (
|
|||||||
-- Country bootstrap is generated from services/user-service/deploy/data/countries.json.
|
-- Country bootstrap is generated from services/user-service/deploy/data/countries.json.
|
||||||
-- Region bootstrap uses product-owned business buckets instead of source geography.
|
-- Region bootstrap uses product-owned business buckets instead of source geography.
|
||||||
INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, country_display_name, phone_country_code, flag, enabled, sort_order, created_at_ms, updated_at_ms) VALUES
|
INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, country_display_name, phone_country_code, flag, enabled, sort_order, created_at_ms, updated_at_ms) VALUES
|
||||||
('Andorra', 'AD', 'AND', '020', '安道尔', '+376', '🇦🇩', TRUE, 10, 0, 0),
|
|
||||||
('United Arab Emirates', 'AE', 'ARE', '784', '阿拉伯联合酋长国', '+971', '🇦🇪', TRUE, 20, 0, 0),
|
('United Arab Emirates', 'AE', 'ARE', '784', '阿拉伯联合酋长国', '+971', '🇦🇪', TRUE, 20, 0, 0),
|
||||||
('Afghanistan', 'AF', 'AFG', '004', '阿富汗', '+93', '🇦🇫', TRUE, 30, 0, 0),
|
('Afghanistan', 'AF', 'AFG', '004', '阿富汗', '+93', '🇦🇫', TRUE, 30, 0, 0),
|
||||||
('Anguilla', 'AI', 'AIA', '660', '安圭拉', '+1', '🇦🇮', TRUE, 50, 0, 0),
|
|
||||||
('Albania', 'AL', 'ALB', '008', '阿尔巴尼亚', '+355', '🇦🇱', TRUE, 60, 0, 0),
|
('Albania', 'AL', 'ALB', '008', '阿尔巴尼亚', '+355', '🇦🇱', TRUE, 60, 0, 0),
|
||||||
('Armenia', 'AM', 'ARM', '051', '亚美尼亚', '+374', '🇦🇲', TRUE, 70, 0, 0),
|
('Armenia', 'AM', 'ARM', '051', '亚美尼亚', '+374', '🇦🇲', TRUE, 70, 0, 0),
|
||||||
('Angola', 'AO', 'AGO', '024', '安哥拉', '+244', '🇦🇴', TRUE, 80, 0, 0),
|
('Angola', 'AO', 'AGO', '024', '安哥拉', '+244', '🇦🇴', TRUE, 80, 0, 0),
|
||||||
('Argentina', 'AR', 'ARG', '032', '阿根廷', '+54', '🇦🇷', TRUE, 100, 0, 0),
|
('Argentina', 'AR', 'ARG', '032', '阿根廷', '+54', '🇦🇷', TRUE, 100, 0, 0),
|
||||||
('Austria', 'AT', 'AUT', '040', '奥地利', '+43', '🇦🇹', TRUE, 120, 0, 0),
|
('Austria', 'AT', 'AUT', '040', '奥地利', '+43', '🇦🇹', TRUE, 120, 0, 0),
|
||||||
('Australia', 'AU', 'AUS', '036', '澳大利亚', '+61', '🇦🇺', TRUE, 130, 0, 0),
|
('Australia', 'AU', 'AUS', '036', '澳大利亚', '+61', '🇦🇺', TRUE, 130, 0, 0),
|
||||||
('Aruba', 'AW', 'ABW', '533', '阿鲁巴', '+297', '🇦🇼', TRUE, 140, 0, 0),
|
|
||||||
('Åland Islands', 'AX', 'ALA', '248', '奥兰群岛', '+358', '🇦🇽', TRUE, 150, 0, 0),
|
|
||||||
('Azerbaijan', 'AZ', 'AZE', '031', '阿塞拜疆', '+994', '🇦🇿', TRUE, 160, 0, 0),
|
|
||||||
('Barbados', 'BB', 'BRB', '052', '巴巴多斯', '+1', '🇧🇧', TRUE, 180, 0, 0),
|
|
||||||
('Bangladesh', 'BD', 'BGD', '050', '孟加拉国', '+880', '🇧🇩', TRUE, 190, 0, 0),
|
('Bangladesh', 'BD', 'BGD', '050', '孟加拉国', '+880', '🇧🇩', TRUE, 190, 0, 0),
|
||||||
('Belgium', 'BE', 'BEL', '056', '比利时', '+32', '🇧🇪', TRUE, 200, 0, 0),
|
('Belgium', 'BE', 'BEL', '056', '比利时', '+32', '🇧🇪', TRUE, 200, 0, 0),
|
||||||
('Burkina Faso', 'BF', 'BFA', '854', '布基纳法索', '+226', '🇧🇫', TRUE, 210, 0, 0),
|
|
||||||
('Bulgaria', 'BG', 'BGR', '100', '保加利亚', '+359', '🇧🇬', TRUE, 220, 0, 0),
|
('Bulgaria', 'BG', 'BGR', '100', '保加利亚', '+359', '🇧🇬', TRUE, 220, 0, 0),
|
||||||
('Bahrain', 'BH', 'BHR', '048', '巴林', '+973', '🇧🇭', TRUE, 230, 0, 0),
|
('Bahrain', 'BH', 'BHR', '048', '巴林', '+973', '🇧🇭', TRUE, 230, 0, 0),
|
||||||
('Burundi', 'BI', 'BDI', '108', '布隆迪', '+257', '🇧🇮', TRUE, 240, 0, 0),
|
|
||||||
('Benin', 'BJ', 'BEN', '204', '贝宁', '+229', '🇧🇯', TRUE, 250, 0, 0),
|
('Benin', 'BJ', 'BEN', '204', '贝宁', '+229', '🇧🇯', TRUE, 250, 0, 0),
|
||||||
('Saint Barthélemy', 'BL', 'BLM', '652', '圣巴泰勒米', '+590', '🇧🇱', TRUE, 260, 0, 0),
|
|
||||||
('Bermuda', 'BM', 'BMU', '060', '百慕大', '+1', '🇧🇲', TRUE, 270, 0, 0),
|
|
||||||
('Brunei', 'BN', 'BRN', '096', '文莱', '+673', '🇧🇳', TRUE, 280, 0, 0),
|
|
||||||
('Bolivia', 'BO', 'BOL', '068', '玻利维亚', '+591', '🇧🇴', TRUE, 290, 0, 0),
|
('Bolivia', 'BO', 'BOL', '068', '玻利维亚', '+591', '🇧🇴', TRUE, 290, 0, 0),
|
||||||
('Brazil', 'BR', 'BRA', '076', '巴西', '+55', '🇧🇷', TRUE, 310, 0, 0),
|
('Brazil', 'BR', 'BRA', '076', '巴西', '+55', '🇧🇷', TRUE, 310, 0, 0),
|
||||||
('Bahamas', 'BS', 'BHS', '044', '巴哈马', '+1', '🇧🇸', TRUE, 320, 0, 0),
|
|
||||||
('Bhutan', 'BT', 'BTN', '064', '不丹', '+975', '🇧🇹', TRUE, 330, 0, 0),
|
|
||||||
('Bouvet Island', 'BV', 'BVT', '074', '布维岛', NULL, '🇧🇻', TRUE, 340, 0, 0),
|
|
||||||
('Botswana', 'BW', 'BWA', '072', '博茨瓦纳', '+267', '🇧🇼', TRUE, 350, 0, 0),
|
('Botswana', 'BW', 'BWA', '072', '博茨瓦纳', '+267', '🇧🇼', TRUE, 350, 0, 0),
|
||||||
('Belarus', 'BY', 'BLR', '112', '白俄罗斯', '+375', '🇧🇾', TRUE, 360, 0, 0),
|
('Belarus', 'BY', 'BLR', '112', '白俄罗斯', '+375', '🇧🇾', TRUE, 360, 0, 0),
|
||||||
('Belize', 'BZ', 'BLZ', '084', '伯利兹', '+501', '🇧🇿', TRUE, 370, 0, 0),
|
|
||||||
('Canada', 'CA', 'CAN', '124', '加拿大', '+1', '🇨🇦', TRUE, 380, 0, 0),
|
('Canada', 'CA', 'CAN', '124', '加拿大', '+1', '🇨🇦', TRUE, 380, 0, 0),
|
||||||
('Cocos (Keeling) Islands', 'CC', 'CCK', '166', '科科斯', '+61', '🇨🇨', TRUE, 390, 0, 0),
|
|
||||||
('DR Congo', 'CD', 'COD', '180', '民主刚果', '+243', '🇨🇩', TRUE, 400, 0, 0),
|
('DR Congo', 'CD', 'COD', '180', '民主刚果', '+243', '🇨🇩', TRUE, 400, 0, 0),
|
||||||
('Central African Republic', 'CF', 'CAF', '140', '中非共和国', '+236', '🇨🇫', TRUE, 410, 0, 0),
|
('Central African Republic', 'CF', 'CAF', '140', '中非共和国', '+236', '🇨🇫', TRUE, 410, 0, 0),
|
||||||
('Congo', 'CG', 'COG', '178', '刚果', '+242', '🇨🇬', TRUE, 420, 0, 0),
|
('Congo', 'CG', 'COG', '178', '刚果', '+242', '🇨🇬', TRUE, 420, 0, 0),
|
||||||
@ -663,19 +647,14 @@ INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, coun
|
|||||||
('Ivory Coast', 'CI', 'CIV', '384', '科特迪瓦', '+225', '🇨🇮', TRUE, 440, 0, 0),
|
('Ivory Coast', 'CI', 'CIV', '384', '科特迪瓦', '+225', '🇨🇮', TRUE, 440, 0, 0),
|
||||||
('Chile', 'CL', 'CHL', '152', '智利', '+56', '🇨🇱', TRUE, 460, 0, 0),
|
('Chile', 'CL', 'CHL', '152', '智利', '+56', '🇨🇱', TRUE, 460, 0, 0),
|
||||||
('Cameroon', 'CM', 'CMR', '120', '喀麦隆', '+237', '🇨🇲', TRUE, 470, 0, 0),
|
('Cameroon', 'CM', 'CMR', '120', '喀麦隆', '+237', '🇨🇲', TRUE, 470, 0, 0),
|
||||||
('China', 'CN', 'CHN', '156', '中国', '+86', '🇨🇳', TRUE, 480, 0, 0),
|
|
||||||
('Colombia', 'CO', 'COL', '170', '哥伦比亚', '+57', '🇨🇴', TRUE, 490, 0, 0),
|
('Colombia', 'CO', 'COL', '170', '哥伦比亚', '+57', '🇨🇴', TRUE, 490, 0, 0),
|
||||||
('Costa Rica', 'CR', 'CRI', '188', '哥斯达黎加', '+506', '🇨🇷', TRUE, 500, 0, 0),
|
('Costa Rica', 'CR', 'CRI', '188', '哥斯达黎加', '+506', '🇨🇷', TRUE, 500, 0, 0),
|
||||||
('Cuba', 'CU', 'CUB', '192', '古巴', '+53', '🇨🇺', TRUE, 510, 0, 0),
|
('Cuba', 'CU', 'CUB', '192', '古巴', '+53', '🇨🇺', TRUE, 510, 0, 0),
|
||||||
('Cape Verde', 'CV', 'CPV', '132', '佛得角', '+238', '🇨🇻', TRUE, 520, 0, 0),
|
|
||||||
('Curaçao', 'CW', 'CUW', '531', '库拉索', '+599', '🇨🇼', TRUE, 530, 0, 0),
|
|
||||||
('Christmas Island', 'CX', 'CXR', '162', '圣诞岛', '+61', '🇨🇽', TRUE, 540, 0, 0),
|
|
||||||
('Cyprus', 'CY', 'CYP', '196', '塞浦路斯', '+357', '🇨🇾', TRUE, 550, 0, 0),
|
('Cyprus', 'CY', 'CYP', '196', '塞浦路斯', '+357', '🇨🇾', TRUE, 550, 0, 0),
|
||||||
('Czechia', 'CZ', 'CZE', '203', '捷克', '+420', '🇨🇿', TRUE, 560, 0, 0),
|
('Czechia', 'CZ', 'CZE', '203', '捷克', '+420', '🇨🇿', TRUE, 560, 0, 0),
|
||||||
('Germany', 'DE', 'DEU', '276', '德国', '+49', '🇩🇪', TRUE, 570, 0, 0),
|
('Germany', 'DE', 'DEU', '276', '德国', '+49', '🇩🇪', TRUE, 570, 0, 0),
|
||||||
('Djibouti', 'DJ', 'DJI', '262', '吉布提', '+253', '🇩🇯', TRUE, 580, 0, 0),
|
('Djibouti', 'DJ', 'DJI', '262', '吉布提', '+253', '🇩🇯', TRUE, 580, 0, 0),
|
||||||
('Denmark', 'DK', 'DNK', '208', '丹麦', '+45', '🇩🇰', TRUE, 590, 0, 0),
|
('Denmark', 'DK', 'DNK', '208', '丹麦', '+45', '🇩🇰', TRUE, 590, 0, 0),
|
||||||
('Dominica', 'DM', 'DMA', '212', '多米尼加', '+1', '🇩🇲', TRUE, 600, 0, 0),
|
|
||||||
('Dominican Republic', 'DO', 'DOM', '214', '多明尼加', '+1', '🇩🇴', TRUE, 610, 0, 0),
|
('Dominican Republic', 'DO', 'DOM', '214', '多明尼加', '+1', '🇩🇴', TRUE, 610, 0, 0),
|
||||||
('Algeria', 'DZ', 'DZA', '012', '阿尔及利亚', '+213', '🇩🇿', TRUE, 620, 0, 0),
|
('Algeria', 'DZ', 'DZA', '012', '阿尔及利亚', '+213', '🇩🇿', TRUE, 620, 0, 0),
|
||||||
('Ecuador', 'EC', 'ECU', '218', '厄瓜多尔', '+593', '🇪🇨', TRUE, 630, 0, 0),
|
('Ecuador', 'EC', 'ECU', '218', '厄瓜多尔', '+593', '🇪🇨', TRUE, 630, 0, 0),
|
||||||
@ -685,26 +664,16 @@ INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, coun
|
|||||||
('Spain', 'ES', 'ESP', '724', '西班牙', '+34', '🇪🇸', TRUE, 680, 0, 0),
|
('Spain', 'ES', 'ESP', '724', '西班牙', '+34', '🇪🇸', TRUE, 680, 0, 0),
|
||||||
('Ethiopia', 'ET', 'ETH', '231', '埃塞俄比亚', '+251', '🇪🇹', TRUE, 690, 0, 0),
|
('Ethiopia', 'ET', 'ETH', '231', '埃塞俄比亚', '+251', '🇪🇹', TRUE, 690, 0, 0),
|
||||||
('Finland', 'FI', 'FIN', '246', '芬兰', '+358', '🇫🇮', TRUE, 700, 0, 0),
|
('Finland', 'FI', 'FIN', '246', '芬兰', '+358', '🇫🇮', TRUE, 700, 0, 0),
|
||||||
('Fiji', 'FJ', 'FJI', '242', '斐济', '+679', '🇫🇯', TRUE, 710, 0, 0),
|
|
||||||
('Micronesia', 'FM', 'FSM', '583', '密克罗尼西亚', '+691', '🇫🇲', TRUE, 730, 0, 0),
|
|
||||||
('France', 'FR', 'FRA', '250', '法国', '+33', '🇫🇷', TRUE, 750, 0, 0),
|
('France', 'FR', 'FRA', '250', '法国', '+33', '🇫🇷', TRUE, 750, 0, 0),
|
||||||
('Gabon', 'GA', 'GAB', '266', '加蓬', '+241', '🇬🇦', TRUE, 760, 0, 0),
|
('Gabon', 'GA', 'GAB', '266', '加蓬', '+241', '🇬🇦', TRUE, 760, 0, 0),
|
||||||
('United Kingdom', 'GB', 'GBR', '826', '英国', '+44', '🇬🇧', TRUE, 770, 0, 0),
|
('United Kingdom', 'GB', 'GBR', '826', '英国', '+44', '🇬🇧', TRUE, 770, 0, 0),
|
||||||
('Grenada', 'GD', 'GRD', '308', '格林纳达', '+1', '🇬🇩', TRUE, 780, 0, 0),
|
|
||||||
('Georgia', 'GE', 'GEO', '268', '格鲁吉亚', '+995', '🇬🇪', TRUE, 790, 0, 0),
|
('Georgia', 'GE', 'GEO', '268', '格鲁吉亚', '+995', '🇬🇪', TRUE, 790, 0, 0),
|
||||||
('French Guiana', 'GF', 'GUF', '254', '法属圭亚那', '+594', '🇬🇫', TRUE, 800, 0, 0),
|
|
||||||
('Guernsey', 'GG', 'GGY', '831', '根西岛', '+44', '🇬🇬', TRUE, 810, 0, 0),
|
|
||||||
('Ghana', 'GH', 'GHA', '288', '加纳', '+233', '🇬🇭', TRUE, 820, 0, 0),
|
('Ghana', 'GH', 'GHA', '288', '加纳', '+233', '🇬🇭', TRUE, 820, 0, 0),
|
||||||
('Gibraltar', 'GI', 'GIB', '292', '直布罗陀', '+350', '🇬🇮', TRUE, 830, 0, 0),
|
|
||||||
('Greenland', 'GL', 'GRL', '304', '格陵兰', '+299', '🇬🇱', TRUE, 840, 0, 0),
|
|
||||||
('Gambia', 'GM', 'GMB', '270', '冈比亚', '+220', '🇬🇲', TRUE, 850, 0, 0),
|
('Gambia', 'GM', 'GMB', '270', '冈比亚', '+220', '🇬🇲', TRUE, 850, 0, 0),
|
||||||
('Guinea', 'GN', 'GIN', '324', '几内亚', '+224', '🇬🇳', TRUE, 860, 0, 0),
|
('Guinea', 'GN', 'GIN', '324', '几内亚', '+224', '🇬🇳', TRUE, 860, 0, 0),
|
||||||
('Equatorial Guinea', 'GQ', 'GNQ', '226', '赤道几内亚', '+240', '🇬🇶', TRUE, 880, 0, 0),
|
('Equatorial Guinea', 'GQ', 'GNQ', '226', '赤道几内亚', '+240', '🇬🇶', TRUE, 880, 0, 0),
|
||||||
('Greece', 'GR', 'GRC', '300', '希腊', '+30', '🇬🇷', TRUE, 890, 0, 0),
|
('Greece', 'GR', 'GRC', '300', '希腊', '+30', '🇬🇷', TRUE, 890, 0, 0),
|
||||||
('South Georgia', 'GS', 'SGS', '239', '南乔治亚', NULL, '🇬🇸', TRUE, 900, 0, 0),
|
|
||||||
('Guatemala', 'GT', 'GTM', '320', '危地马拉', '+502', '🇬🇹', TRUE, 910, 0, 0),
|
('Guatemala', 'GT', 'GTM', '320', '危地马拉', '+502', '🇬🇹', TRUE, 910, 0, 0),
|
||||||
('Guam', 'GU', 'GUM', '316', '关岛', '+1', '🇬🇺', TRUE, 920, 0, 0),
|
|
||||||
('Guyana', 'GY', 'GUY', '328', '圭亚那', '+592', '🇬🇾', TRUE, 940, 0, 0),
|
|
||||||
('Honduras', 'HN', 'HND', '340', '洪都拉斯', '+504', '🇭🇳', TRUE, 970, 0, 0),
|
('Honduras', 'HN', 'HND', '340', '洪都拉斯', '+504', '🇭🇳', TRUE, 970, 0, 0),
|
||||||
('Croatia', 'HR', 'HRV', '191', '克罗地亚', '+385', '🇭🇷', TRUE, 980, 0, 0),
|
('Croatia', 'HR', 'HRV', '191', '克罗地亚', '+385', '🇭🇷', TRUE, 980, 0, 0),
|
||||||
('Haiti', 'HT', 'HTI', '332', '海地', '+509', '🇭🇹', TRUE, 990, 0, 0),
|
('Haiti', 'HT', 'HTI', '332', '海地', '+509', '🇭🇹', TRUE, 990, 0, 0),
|
||||||
@ -712,68 +681,42 @@ INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, coun
|
|||||||
('Indonesia', 'ID', 'IDN', '360', '印度尼西亚', '+62', '🇮🇩', TRUE, 1010, 0, 0),
|
('Indonesia', 'ID', 'IDN', '360', '印度尼西亚', '+62', '🇮🇩', TRUE, 1010, 0, 0),
|
||||||
('Ireland', 'IE', 'IRL', '372', '爱尔兰', '+353', '🇮🇪', TRUE, 1020, 0, 0),
|
('Ireland', 'IE', 'IRL', '372', '爱尔兰', '+353', '🇮🇪', TRUE, 1020, 0, 0),
|
||||||
('Israel', 'IL', 'ISR', '376', '以色列', '+972', '🇮🇱', TRUE, 1030, 0, 0),
|
('Israel', 'IL', 'ISR', '376', '以色列', '+972', '🇮🇱', TRUE, 1030, 0, 0),
|
||||||
('Isle of Man', 'IM', 'IMN', '833', '马恩岛', '+44', '🇮🇲', TRUE, 1040, 0, 0),
|
|
||||||
('India', 'IN', 'IND', '356', '印度', '+91', '🇮🇳', TRUE, 1050, 0, 0),
|
('India', 'IN', 'IND', '356', '印度', '+91', '🇮🇳', TRUE, 1050, 0, 0),
|
||||||
('Iraq', 'IQ', 'IRQ', '368', '伊拉克', '+964', '🇮🇶', TRUE, 1070, 0, 0),
|
('Iraq', 'IQ', 'IRQ', '368', '伊拉克', '+964', '🇮🇶', TRUE, 1070, 0, 0),
|
||||||
('Iran', 'IR', 'IRN', '364', '伊朗', '+98', '🇮🇷', TRUE, 1080, 0, 0),
|
('Iran', 'IR', 'IRN', '364', '伊朗', '+98', '🇮🇷', TRUE, 1080, 0, 0),
|
||||||
('Iceland', 'IS', 'ISL', '352', '冰岛', '+354', '🇮🇸', TRUE, 1090, 0, 0),
|
|
||||||
('Italy', 'IT', 'ITA', '380', '意大利', '+39', '🇮🇹', TRUE, 1100, 0, 0),
|
('Italy', 'IT', 'ITA', '380', '意大利', '+39', '🇮🇹', TRUE, 1100, 0, 0),
|
||||||
('Jersey', 'JE', 'JEY', '832', '泽西岛', '+44', '🇯🇪', TRUE, 1110, 0, 0),
|
|
||||||
('Jamaica', 'JM', 'JAM', '388', '牙买加', '+1', '🇯🇲', TRUE, 1120, 0, 0),
|
('Jamaica', 'JM', 'JAM', '388', '牙买加', '+1', '🇯🇲', TRUE, 1120, 0, 0),
|
||||||
('Jordan', 'JO', 'JOR', '400', '约旦', '+962', '🇯🇴', TRUE, 1130, 0, 0),
|
('Jordan', 'JO', 'JOR', '400', '约旦', '+962', '🇯🇴', TRUE, 1130, 0, 0),
|
||||||
('Japan', 'JP', 'JPN', '392', '日本', '+81', '🇯🇵', TRUE, 1140, 0, 0),
|
('Japan', 'JP', 'JPN', '392', '日本', '+81', '🇯🇵', TRUE, 1140, 0, 0),
|
||||||
('Kenya', 'KE', 'KEN', '404', '肯尼亚', '+254', '🇰🇪', TRUE, 1150, 0, 0),
|
('Kenya', 'KE', 'KEN', '404', '肯尼亚', '+254', '🇰🇪', TRUE, 1150, 0, 0),
|
||||||
('Kyrgyzstan', 'KG', 'KGZ', '417', '吉尔吉斯斯坦', '+996', '🇰🇬', TRUE, 1160, 0, 0),
|
('Kyrgyzstan', 'KG', 'KGZ', '417', '吉尔吉斯斯坦', '+996', '🇰🇬', TRUE, 1160, 0, 0),
|
||||||
('Cambodia', 'KH', 'KHM', '116', '柬埔寨', '+855', '🇰🇭', TRUE, 1170, 0, 0),
|
('Cambodia', 'KH', 'KHM', '116', '柬埔寨', '+855', '🇰🇭', TRUE, 1170, 0, 0),
|
||||||
('Kiribati', 'KI', 'KIR', '296', '基里巴斯', '+686', '🇰🇮', TRUE, 1180, 0, 0),
|
|
||||||
('Comoros', 'KM', 'COM', '174', '科摩罗', '+269', '🇰🇲', TRUE, 1190, 0, 0),
|
|
||||||
('North Korea', 'KP', 'PRK', '408', '朝鲜', '+850', '🇰🇵', TRUE, 1210, 0, 0),
|
('North Korea', 'KP', 'PRK', '408', '朝鲜', '+850', '🇰🇵', TRUE, 1210, 0, 0),
|
||||||
('South Korea', 'KR', 'KOR', '410', '韩国', '+82', '🇰🇷', TRUE, 1220, 0, 0),
|
('South Korea', 'KR', 'KOR', '410', '韩国', '+82', '🇰🇷', TRUE, 1220, 0, 0),
|
||||||
('Kuwait', 'KW', 'KWT', '414', '科威特', '+965', '🇰🇼', TRUE, 1230, 0, 0),
|
('Kuwait', 'KW', 'KWT', '414', '科威特', '+965', '🇰🇼', TRUE, 1230, 0, 0),
|
||||||
('Cayman Islands', 'KY', 'CYM', '136', '开曼群岛', '+1', '🇰🇾', TRUE, 1240, 0, 0),
|
|
||||||
('Kazakhstan', 'KZ', 'KAZ', '398', '哈萨克斯坦', '+7', '🇰🇿', TRUE, 1250, 0, 0),
|
('Kazakhstan', 'KZ', 'KAZ', '398', '哈萨克斯坦', '+7', '🇰🇿', TRUE, 1250, 0, 0),
|
||||||
('Laos', 'LA', 'LAO', '418', '老挝', '+856', '🇱🇦', TRUE, 1260, 0, 0),
|
('Laos', 'LA', 'LAO', '418', '老挝', '+856', '🇱🇦', TRUE, 1260, 0, 0),
|
||||||
('Lebanon', 'LB', 'LBN', '422', '黎巴嫩', '+961', '🇱🇧', TRUE, 1270, 0, 0),
|
('Lebanon', 'LB', 'LBN', '422', '黎巴嫩', '+961', '🇱🇧', TRUE, 1270, 0, 0),
|
||||||
('Saint Lucia', 'LC', 'LCA', '662', '圣卢西亚', '+1', '🇱🇨', TRUE, 1280, 0, 0),
|
|
||||||
('Liechtenstein', 'LI', 'LIE', '438', '列支敦士登', '+423', '🇱🇮', TRUE, 1290, 0, 0),
|
|
||||||
('Sri Lanka', 'LK', 'LKA', '144', '斯里兰卡', '+94', '🇱🇰', TRUE, 1300, 0, 0),
|
('Sri Lanka', 'LK', 'LKA', '144', '斯里兰卡', '+94', '🇱🇰', TRUE, 1300, 0, 0),
|
||||||
('Liberia', 'LR', 'LBR', '430', '利比里亚', '+231', '🇱🇷', TRUE, 1310, 0, 0),
|
('Liberia', 'LR', 'LBR', '430', '利比里亚', '+231', '🇱🇷', TRUE, 1310, 0, 0),
|
||||||
('Lesotho', 'LS', 'LSO', '426', '莱索托', '+266', '🇱🇸', TRUE, 1320, 0, 0),
|
|
||||||
('Lithuania', 'LT', 'LTU', '440', '立陶宛', '+370', '🇱🇹', TRUE, 1330, 0, 0),
|
|
||||||
('Luxembourg', 'LU', 'LUX', '442', '卢森堡', '+352', '🇱🇺', TRUE, 1340, 0, 0),
|
|
||||||
('Latvia', 'LV', 'LVA', '428', '拉脱维亚', '+371', '🇱🇻', TRUE, 1350, 0, 0),
|
|
||||||
('Libya', 'LY', 'LBY', '434', '利比亚', '+218', '🇱🇾', TRUE, 1360, 0, 0),
|
('Libya', 'LY', 'LBY', '434', '利比亚', '+218', '🇱🇾', TRUE, 1360, 0, 0),
|
||||||
('Morocco', 'MA', 'MAR', '504', '摩洛哥', '+212', '🇲🇦', TRUE, 1370, 0, 0),
|
('Morocco', 'MA', 'MAR', '504', '摩洛哥', '+212', '🇲🇦', TRUE, 1370, 0, 0),
|
||||||
('Monaco', 'MC', 'MCO', '492', '摩纳哥', '+377', '🇲🇨', TRUE, 1380, 0, 0),
|
|
||||||
('Moldova', 'MD', 'MDA', '498', '摩尔多瓦', '+373', '🇲🇩', TRUE, 1390, 0, 0),
|
|
||||||
('Montenegro', 'ME', 'MNE', '499', '黑山', '+382', '🇲🇪', TRUE, 1400, 0, 0),
|
|
||||||
('Saint Martin', 'MF', 'MAF', '663', '圣马丁', '+590', '🇲🇫', TRUE, 1410, 0, 0),
|
|
||||||
('Madagascar', 'MG', 'MDG', '450', '马达加斯加', '+261', '🇲🇬', TRUE, 1420, 0, 0),
|
('Madagascar', 'MG', 'MDG', '450', '马达加斯加', '+261', '🇲🇬', TRUE, 1420, 0, 0),
|
||||||
('Mali', 'ML', 'MLI', '466', '马里', '+223', '🇲🇱', TRUE, 1450, 0, 0),
|
('Mali', 'ML', 'MLI', '466', '马里', '+223', '🇲🇱', TRUE, 1450, 0, 0),
|
||||||
('Myanmar', 'MM', 'MMR', '104', '缅甸', '+95', '🇲🇲', TRUE, 1460, 0, 0),
|
('Myanmar', 'MM', 'MMR', '104', '缅甸', '+95', '🇲🇲', TRUE, 1460, 0, 0),
|
||||||
('Mongolia', 'MN', 'MNG', '496', '蒙古', '+976', '🇲🇳', TRUE, 1470, 0, 0),
|
('Mongolia', 'MN', 'MNG', '496', '蒙古', '+976', '🇲🇳', TRUE, 1470, 0, 0),
|
||||||
('Macao', 'MO', 'MAC', '446', '中国澳门', '+853', '🇲🇴', TRUE, 1480, 0, 0),
|
|
||||||
('Martinique', 'MQ', 'MTQ', '474', '马提尼克', '+596', '🇲🇶', TRUE, 1500, 0, 0),
|
|
||||||
('Mauritania', 'MR', 'MRT', '478', '毛里塔尼亚', '+222', '🇲🇷', TRUE, 1510, 0, 0),
|
|
||||||
('Montserrat', 'MS', 'MSR', '500', '蒙特塞拉特', '+1', '🇲🇸', TRUE, 1520, 0, 0),
|
|
||||||
('Malta', 'MT', 'MLT', '470', '马耳他', '+356', '🇲🇹', TRUE, 1530, 0, 0),
|
|
||||||
('Mauritius', 'MU', 'MUS', '480', '毛里求斯', '+230', '🇲🇺', TRUE, 1540, 0, 0),
|
('Mauritius', 'MU', 'MUS', '480', '毛里求斯', '+230', '🇲🇺', TRUE, 1540, 0, 0),
|
||||||
('Maldives', 'MV', 'MDV', '462', '马尔代夫', '+960', '🇲🇻', TRUE, 1550, 0, 0),
|
('Maldives', 'MV', 'MDV', '462', '马尔代夫', '+960', '🇲🇻', TRUE, 1550, 0, 0),
|
||||||
('Malawi', 'MW', 'MWI', '454', '马拉维', '+265', '🇲🇼', TRUE, 1560, 0, 0),
|
|
||||||
('Mexico', 'MX', 'MEX', '484', '墨西哥', '+52', '🇲🇽', TRUE, 1570, 0, 0),
|
('Mexico', 'MX', 'MEX', '484', '墨西哥', '+52', '🇲🇽', TRUE, 1570, 0, 0),
|
||||||
('Malaysia', 'MY', 'MYS', '458', '马来西亚', '+60', '🇲🇾', TRUE, 1580, 0, 0),
|
('Malaysia', 'MY', 'MYS', '458', '马来西亚', '+60', '🇲🇾', TRUE, 1580, 0, 0),
|
||||||
('Mozambique', 'MZ', 'MOZ', '508', '莫桑比克', '+258', '🇲🇿', TRUE, 1590, 0, 0),
|
('Mozambique', 'MZ', 'MOZ', '508', '莫桑比克', '+258', '🇲🇿', TRUE, 1590, 0, 0),
|
||||||
('Namibia', 'NA', 'NAM', '516', '纳米比亚', '+264', '🇳🇦', TRUE, 1600, 0, 0),
|
('Namibia', 'NA', 'NAM', '516', '纳米比亚', '+264', '🇳🇦', TRUE, 1600, 0, 0),
|
||||||
('New Caledonia', 'NC', 'NCL', '540', '新喀里多尼亚', '+687', '🇳🇨', TRUE, 1610, 0, 0),
|
|
||||||
('Niger', 'NE', 'NER', '562', '尼日尔', '+227', '🇳🇪', TRUE, 1620, 0, 0),
|
('Niger', 'NE', 'NER', '562', '尼日尔', '+227', '🇳🇪', TRUE, 1620, 0, 0),
|
||||||
('Norfolk Island', 'NF', 'NFK', '574', '诺福克岛', '+672', '🇳🇫', TRUE, 1630, 0, 0),
|
|
||||||
('Nigeria', 'NG', 'NGA', '566', '尼日利亚', '+234', '🇳🇬', TRUE, 1640, 0, 0),
|
('Nigeria', 'NG', 'NGA', '566', '尼日利亚', '+234', '🇳🇬', TRUE, 1640, 0, 0),
|
||||||
('Nicaragua', 'NI', 'NIC', '558', '尼加拉瓜', '+505', '🇳🇮', TRUE, 1650, 0, 0),
|
('Nicaragua', 'NI', 'NIC', '558', '尼加拉瓜', '+505', '🇳🇮', TRUE, 1650, 0, 0),
|
||||||
('Netherlands', 'NL', 'NLD', '528', '荷兰', '+31', '🇳🇱', TRUE, 1660, 0, 0),
|
('Netherlands', 'NL', 'NLD', '528', '荷兰', '+31', '🇳🇱', TRUE, 1660, 0, 0),
|
||||||
('Norway', 'NO', 'NOR', '578', '挪威', '+47', '🇳🇴', TRUE, 1670, 0, 0),
|
('Norway', 'NO', 'NOR', '578', '挪威', '+47', '🇳🇴', TRUE, 1670, 0, 0),
|
||||||
('Nepal', 'NP', 'NPL', '524', '尼泊尔', '+977', '🇳🇵', TRUE, 1680, 0, 0),
|
('Nepal', 'NP', 'NPL', '524', '尼泊尔', '+977', '🇳🇵', TRUE, 1680, 0, 0),
|
||||||
('Nauru', 'NR', 'NRU', '520', '瑙鲁', '+674', '🇳🇷', TRUE, 1690, 0, 0),
|
|
||||||
('Niue', 'NU', 'NIU', '570', '纽埃', '+683', '🇳🇺', TRUE, 1700, 0, 0),
|
|
||||||
('New Zealand', 'NZ', 'NZL', '554', '新西兰', '+64', '🇳🇿', TRUE, 1710, 0, 0),
|
('New Zealand', 'NZ', 'NZL', '554', '新西兰', '+64', '🇳🇿', TRUE, 1710, 0, 0),
|
||||||
('Oman', 'OM', 'OMN', '512', '阿曼', '+968', '🇴🇲', TRUE, 1720, 0, 0),
|
('Oman', 'OM', 'OMN', '512', '阿曼', '+968', '🇴🇲', TRUE, 1720, 0, 0),
|
||||||
('Panama', 'PA', 'PAN', '591', '巴拿马', '+507', '🇵🇦', TRUE, 1730, 0, 0),
|
('Panama', 'PA', 'PAN', '591', '巴拿马', '+507', '🇵🇦', TRUE, 1730, 0, 0),
|
||||||
@ -784,59 +727,39 @@ INSERT INTO countries (country_name, country_code, iso_alpha3, iso_numeric, coun
|
|||||||
('Puerto Rico', 'PR', 'PRI', '630', '波多黎各', '+1', '🇵🇷', TRUE, 1820, 0, 0),
|
('Puerto Rico', 'PR', 'PRI', '630', '波多黎各', '+1', '🇵🇷', TRUE, 1820, 0, 0),
|
||||||
('Palestine', 'PS', 'PSE', '275', '巴勒斯坦', '+970', '🇵🇸', TRUE, 1830, 0, 0),
|
('Palestine', 'PS', 'PSE', '275', '巴勒斯坦', '+970', '🇵🇸', TRUE, 1830, 0, 0),
|
||||||
('Portugal', 'PT', 'PRT', '620', '葡萄牙', '+351', '🇵🇹', TRUE, 1840, 0, 0),
|
('Portugal', 'PT', 'PRT', '620', '葡萄牙', '+351', '🇵🇹', TRUE, 1840, 0, 0),
|
||||||
('Palau', 'PW', 'PLW', '585', '帕劳', '+680', '🇵🇼', TRUE, 1850, 0, 0),
|
|
||||||
('Paraguay', 'PY', 'PRY', '600', '巴拉圭', '+595', '🇵🇾', TRUE, 1860, 0, 0),
|
('Paraguay', 'PY', 'PRY', '600', '巴拉圭', '+595', '🇵🇾', TRUE, 1860, 0, 0),
|
||||||
('Qatar', 'QA', 'QAT', '634', '卡塔尔', '+974', '🇶🇦', TRUE, 1870, 0, 0),
|
('Qatar', 'QA', 'QAT', '634', '卡塔尔', '+974', '🇶🇦', TRUE, 1870, 0, 0),
|
||||||
('Réunion', 'RE', 'REU', '638', '留尼旺岛', '+262', '🇷🇪', TRUE, 1880, 0, 0),
|
|
||||||
('Romania', 'RO', 'ROU', '642', '罗马尼亚', '+40', '🇷🇴', TRUE, 1890, 0, 0),
|
|
||||||
('Serbia', 'RS', 'SRB', '688', '塞尔维亚', '+381', '🇷🇸', TRUE, 1900, 0, 0),
|
('Serbia', 'RS', 'SRB', '688', '塞尔维亚', '+381', '🇷🇸', TRUE, 1900, 0, 0),
|
||||||
('Russia', 'RU', 'RUS', '643', '俄罗斯', '+7', '🇷🇺', TRUE, 1910, 0, 0),
|
('Russia', 'RU', 'RUS', '643', '俄罗斯', '+7', '🇷🇺', TRUE, 1910, 0, 0),
|
||||||
('Rwanda', 'RW', 'RWA', '646', '卢旺达', '+250', '🇷🇼', TRUE, 1920, 0, 0),
|
('Rwanda', 'RW', 'RWA', '646', '卢旺达', '+250', '🇷🇼', TRUE, 1920, 0, 0),
|
||||||
('Saudi Arabia', 'SA', 'SAU', '682', '沙特阿拉伯', '+966', '🇸🇦', TRUE, 1930, 0, 0),
|
('Saudi Arabia', 'SA', 'SAU', '682', '沙特阿拉伯', '+966', '🇸🇦', TRUE, 1930, 0, 0),
|
||||||
('Solomon Islands', 'SB', 'SLB', '090', '所罗门群岛', '+677', '🇸🇧', TRUE, 1940, 0, 0),
|
|
||||||
('Seychelles', 'SC', 'SYC', '690', '塞舌尔', '+248', '🇸🇨', TRUE, 1950, 0, 0),
|
|
||||||
('Sudan', 'SD', 'SDN', '729', '苏丹', '+249', '🇸🇩', TRUE, 1960, 0, 0),
|
('Sudan', 'SD', 'SDN', '729', '苏丹', '+249', '🇸🇩', TRUE, 1960, 0, 0),
|
||||||
('Sweden', 'SE', 'SWE', '752', '瑞典', '+46', '🇸🇪', TRUE, 1970, 0, 0),
|
('Sweden', 'SE', 'SWE', '752', '瑞典', '+46', '🇸🇪', TRUE, 1970, 0, 0),
|
||||||
('Singapore', 'SG', 'SGP', '702', '新加坡', '+65', '🇸🇬', TRUE, 1980, 0, 0),
|
('Singapore', 'SG', 'SGP', '702', '新加坡', '+65', '🇸🇬', TRUE, 1980, 0, 0),
|
||||||
('Slovenia', 'SI', 'SVN', '705', '斯洛文尼亚', '+386', '🇸🇮', TRUE, 2000, 0, 0),
|
|
||||||
('Svalbard and Jan Mayen', 'SJ', 'SJM', '744', '斯瓦尔巴特', '+47', '🇸🇯', TRUE, 2010, 0, 0),
|
|
||||||
('Slovakia', 'SK', 'SVK', '703', '斯洛伐克', '+421', '🇸🇰', TRUE, 2020, 0, 0),
|
('Slovakia', 'SK', 'SVK', '703', '斯洛伐克', '+421', '🇸🇰', TRUE, 2020, 0, 0),
|
||||||
('Sierra Leone', 'SL', 'SLE', '694', '塞拉利昂', '+232', '🇸🇱', TRUE, 2030, 0, 0),
|
('Sierra Leone', 'SL', 'SLE', '694', '塞拉利昂', '+232', '🇸🇱', TRUE, 2030, 0, 0),
|
||||||
('San Marino', 'SM', 'SMR', '674', '圣马力诺', '+378', '🇸🇲', TRUE, 2040, 0, 0),
|
|
||||||
('Senegal', 'SN', 'SEN', '686', '塞内加尔', '+221', '🇸🇳', TRUE, 2050, 0, 0),
|
('Senegal', 'SN', 'SEN', '686', '塞内加尔', '+221', '🇸🇳', TRUE, 2050, 0, 0),
|
||||||
('Somalia', 'SO', 'SOM', '706', '索马里', '+252', '🇸🇴', TRUE, 2060, 0, 0),
|
('Somalia', 'SO', 'SOM', '706', '索马里', '+252', '🇸🇴', TRUE, 2060, 0, 0),
|
||||||
('Suriname', 'SR', 'SUR', '740', '苏里南', '+597', '🇸🇷', TRUE, 2070, 0, 0),
|
|
||||||
('South Sudan', 'SS', 'SSD', '728', '南苏丹', '+211', '🇸🇸', TRUE, 2080, 0, 0),
|
('South Sudan', 'SS', 'SSD', '728', '南苏丹', '+211', '🇸🇸', TRUE, 2080, 0, 0),
|
||||||
('El Salvador', 'SV', 'SLV', '222', '萨尔瓦多', '+503', '🇸🇻', TRUE, 2100, 0, 0),
|
('El Salvador', 'SV', 'SLV', '222', '萨尔瓦多', '+503', '🇸🇻', TRUE, 2100, 0, 0),
|
||||||
('Sint Maarten', 'SX', 'SXM', '534', '圣马丁岛', '+1', '🇸🇽', TRUE, 2110, 0, 0),
|
|
||||||
('Syria', 'SY', 'SYR', '760', '叙利亚', '+963', '🇸🇾', TRUE, 2120, 0, 0),
|
('Syria', 'SY', 'SYR', '760', '叙利亚', '+963', '🇸🇾', TRUE, 2120, 0, 0),
|
||||||
('Eswatini', 'SZ', 'SWZ', '748', '斯威士兰', '+268', '🇸🇿', TRUE, 2130, 0, 0),
|
('Eswatini', 'SZ', 'SWZ', '748', '斯威士兰', '+268', '🇸🇿', TRUE, 2130, 0, 0),
|
||||||
('Chad', 'TD', 'TCD', '148', '乍得', '+235', '🇹🇩', TRUE, 2150, 0, 0),
|
('Chad', 'TD', 'TCD', '148', '乍得', '+235', '🇹🇩', TRUE, 2150, 0, 0),
|
||||||
('Togo', 'TG', 'TGO', '768', '多哥', '+228', '🇹🇬', TRUE, 2170, 0, 0),
|
|
||||||
('Thailand', 'TH', 'THA', '764', '泰国', '+66', '🇹🇭', TRUE, 2180, 0, 0),
|
('Thailand', 'TH', 'THA', '764', '泰国', '+66', '🇹🇭', TRUE, 2180, 0, 0),
|
||||||
('Tajikistan', 'TJ', 'TJK', '762', '塔吉克斯坦', '+992', '🇹🇯', TRUE, 2190, 0, 0),
|
('Tajikistan', 'TJ', 'TJK', '762', '塔吉克斯坦', '+992', '🇹🇯', TRUE, 2190, 0, 0),
|
||||||
('Tokelau', 'TK', 'TKL', '772', '托克劳', '+690', '🇹🇰', TRUE, 2200, 0, 0),
|
|
||||||
('Timor-Leste', 'TL', 'TLS', '626', '东帝汶', '+670', '🇹🇱', TRUE, 2210, 0, 0),
|
|
||||||
('Turkmenistan', 'TM', 'TKM', '795', '土库曼斯坦', '+993', '🇹🇲', TRUE, 2220, 0, 0),
|
('Turkmenistan', 'TM', 'TKM', '795', '土库曼斯坦', '+993', '🇹🇲', TRUE, 2220, 0, 0),
|
||||||
('Tunisia', 'TN', 'TUN', '788', '突尼斯', '+216', '🇹🇳', TRUE, 2230, 0, 0),
|
('Tunisia', 'TN', 'TUN', '788', '突尼斯', '+216', '🇹🇳', TRUE, 2230, 0, 0),
|
||||||
('Tonga', 'TO', 'TON', '776', '汤加', '+676', '🇹🇴', TRUE, 2240, 0, 0),
|
|
||||||
('Türkiye', 'TR', 'TUR', '792', '土耳其', '+90', '🇹🇷', TRUE, 2250, 0, 0),
|
('Türkiye', 'TR', 'TUR', '792', '土耳其', '+90', '🇹🇷', TRUE, 2250, 0, 0),
|
||||||
('Tuvalu', 'TV', 'TUV', '798', '图瓦卢', '+688', '🇹🇻', TRUE, 2270, 0, 0),
|
|
||||||
('Tanzania', 'TZ', 'TZA', '834', '坦桑尼亚', '+255', '🇹🇿', TRUE, 2290, 0, 0),
|
('Tanzania', 'TZ', 'TZA', '834', '坦桑尼亚', '+255', '🇹🇿', TRUE, 2290, 0, 0),
|
||||||
('Ukraine', 'UA', 'UKR', '804', '乌克兰', '+380', '🇺🇦', TRUE, 2300, 0, 0),
|
('Ukraine', 'UA', 'UKR', '804', '乌克兰', '+380', '🇺🇦', TRUE, 2300, 0, 0),
|
||||||
('Uganda', 'UG', 'UGA', '800', '乌干达', '+256', '🇺🇬', TRUE, 2310, 0, 0),
|
('Uganda', 'UG', 'UGA', '800', '乌干达', '+256', '🇺🇬', TRUE, 2310, 0, 0),
|
||||||
('United States', 'US', 'USA', '840', '美国', '+1', '🇺🇸', TRUE, 2330, 0, 0),
|
('United States', 'US', 'USA', '840', '美国', '+1', '🇺🇸', TRUE, 2330, 0, 0),
|
||||||
('Uruguay', 'UY', 'URY', '858', '乌拉圭', '+598', '🇺🇾', TRUE, 2340, 0, 0),
|
('Uruguay', 'UY', 'URY', '858', '乌拉圭', '+598', '🇺🇾', TRUE, 2340, 0, 0),
|
||||||
('Uzbekistan', 'UZ', 'UZB', '860', '乌兹别克斯坦', '+998', '🇺🇿', TRUE, 2350, 0, 0),
|
('Uzbekistan', 'UZ', 'UZB', '860', '乌兹别克斯坦', '+998', '🇺🇿', TRUE, 2350, 0, 0),
|
||||||
('Vatican City', 'VA', 'VAT', '336', '梵蒂冈', '+39', '🇻🇦', TRUE, 2360, 0, 0),
|
|
||||||
('Venezuela', 'VE', 'VEN', '862', '委内瑞拉', '+58', '🇻🇪', TRUE, 2380, 0, 0),
|
('Venezuela', 'VE', 'VEN', '862', '委内瑞拉', '+58', '🇻🇪', TRUE, 2380, 0, 0),
|
||||||
('Vietnam', 'VN', 'VNM', '704', '越南', '+84', '🇻🇳', TRUE, 2410, 0, 0),
|
('Vietnam', 'VN', 'VNM', '704', '越南', '+84', '🇻🇳', TRUE, 2410, 0, 0),
|
||||||
('Vanuatu', 'VU', 'VUT', '548', '瓦努阿图', '+678', '🇻🇺', TRUE, 2420, 0, 0),
|
|
||||||
('Samoa', 'WS', 'WSM', '882', '萨摩亚', '+685', '🇼🇸', TRUE, 2440, 0, 0),
|
|
||||||
('Kosovo', 'XK', 'UNK', NULL, '科索沃', '+383', '🇽🇰', TRUE, 2450, 0, 0),
|
('Kosovo', 'XK', 'UNK', NULL, '科索沃', '+383', '🇽🇰', TRUE, 2450, 0, 0),
|
||||||
('Yemen', 'YE', 'YEM', '887', '也门', '+967', '🇾🇪', TRUE, 2460, 0, 0),
|
('Yemen', 'YE', 'YEM', '887', '也门', '+967', '🇾🇪', TRUE, 2460, 0, 0),
|
||||||
('Mayotte', 'YT', 'MYT', '175', '马约特', '+262', '🇾🇹', TRUE, 2470, 0, 0),
|
|
||||||
('South Africa', 'ZA', 'ZAF', '710', '南非', '+27', '🇿🇦', TRUE, 2480, 0, 0),
|
|
||||||
('Zambia', 'ZM', 'ZMB', '894', '赞比亚', '+260', '🇿🇲', TRUE, 2490, 0, 0),
|
('Zambia', 'ZM', 'ZMB', '894', '赞比亚', '+260', '🇿🇲', TRUE, 2490, 0, 0),
|
||||||
('Zimbabwe', 'ZW', 'ZWE', '716', '津巴布韦', '+263', '🇿🇼', TRUE, 2500, 0, 0)
|
('Zimbabwe', 'ZW', 'ZWE', '716', '津巴布韦', '+263', '🇿🇼', TRUE, 2500, 0, 0)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
@ -863,14 +786,10 @@ ON DUPLICATE KEY UPDATE
|
|||||||
INSERT INTO region_countries (app_code, region_id, country_code, status, created_at_ms, updated_at_ms)
|
INSERT INTO region_countries (app_code, region_id, country_code, status, created_at_ms, updated_at_ms)
|
||||||
SELECT 'lalu', r.region_id, seed.country_code, 'active', 0, 0
|
SELECT 'lalu', r.region_id, seed.country_code, 'active', 0, 0
|
||||||
FROM (
|
FROM (
|
||||||
SELECT 'AD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'AE' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'AE' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'AF' AS country_code, 'INDIA' AS region_code
|
SELECT 'AF' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'AI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'AL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'AL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'AM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'AM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -883,54 +802,26 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'AU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'AU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'AW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'AX' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'AZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BB' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BD' AS country_code, 'INDIA' AS region_code
|
SELECT 'BD' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BH' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'BH' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BT' AS country_code, 'INDIA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'BW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'BY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'BZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CC' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -945,20 +836,12 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CX' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'CY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'CZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'CZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -969,8 +852,6 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'DK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'DK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'DM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'DO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'DO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'DZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'DZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -989,30 +870,16 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'FI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'FI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'FJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'FM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'FR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'FR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GB' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GB' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1021,14 +888,8 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'GT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'GU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'GY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'HN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'HN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'HR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'HR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1043,20 +904,14 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'IL' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'IL' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'IM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'IN' AS country_code, 'INDIA' AS region_code
|
SELECT 'IN' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'IQ' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'IQ' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'IR' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'IR' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'IS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'IT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'IT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'JE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'JM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'JM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'JO' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'JO' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
@ -1069,52 +924,26 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'KH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'KH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'KI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'KM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'KP' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'KP' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'KR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'KR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'KW' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'KW' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'KY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'KZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'KZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'LA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'LA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'LB' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'LB' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'LC' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LK' AS country_code, 'INDIA' AS region_code
|
SELECT 'LK' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'LR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'LR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'LS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'LY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'LY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MC' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'ME' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'ML' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'ML' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1123,22 +952,10 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MQ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MV' AS country_code, 'INDIA' AS region_code
|
SELECT 'MV' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'MX' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MX' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'MY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'MY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1147,12 +964,8 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'NA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NC' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'NE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'NE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NF' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'NG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'NG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'NI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1163,10 +976,6 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NP' AS country_code, 'INDIA' AS region_code
|
SELECT 'NP' AS country_code, 'INDIA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'NR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'NU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'NZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'NZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'OM' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'OM' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
@ -1187,16 +996,10 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'PT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'PT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'PW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'PY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'PY' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'QA' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'QA' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'RE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'RO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'RS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'RS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'RU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'RU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1205,64 +1008,40 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SA' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'SA' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SB' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SC' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SI' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SR' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SX' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'SY' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'SY' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'SZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'SZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TD' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TG' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TH' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TJ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TL' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TO' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TR' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'TR' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'TV' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'TZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'TZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'UA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'UA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1275,24 +1054,14 @@ FROM (
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'UZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'UZ' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'VA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'VE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'VE' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'VN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'VN' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'VU' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'WS' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'XK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'XK' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'YE' AS country_code, 'MIDDLE_EAST' AS region_code
|
SELECT 'YE' AS country_code, 'MIDDLE_EAST' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'YT' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'ZA' AS country_code, 'EURASIA_AMERICA' AS region_code
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'ZM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'ZM' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT 'ZW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
SELECT 'ZW' AS country_code, 'EURASIA_AMERICA' AS region_code
|
||||||
@ -1312,11 +1081,11 @@ WHERE app_code = 'lalu'
|
|||||||
-- Product-excluded countries are hard removed so reapplying initdb updates existing local databases as well as fresh volumes.
|
-- Product-excluded countries are hard removed so reapplying initdb updates existing local databases as well as fresh volumes.
|
||||||
DELETE FROM region_countries
|
DELETE FROM region_countries
|
||||||
WHERE app_code = 'lalu'
|
WHERE app_code = 'lalu'
|
||||||
AND country_code IN ('AG', 'AQ', 'AS', 'BA', 'BQ', 'CK', 'EH', 'FK', 'FO', 'GP', 'GW', 'HK', 'HM', 'IO', 'KN', 'MH', 'MK', 'MP', 'PF', 'PG', 'PM', 'PN', 'SH', 'ST', 'TC', 'TF', 'TT', 'TW', 'UM', 'VC', 'VG', 'VI', 'WF');
|
AND country_code IN ('AD', 'AG', 'AI', 'AQ', 'AS', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BF', 'BI', 'BL', 'BM', 'BN', 'BQ', 'BS', 'BT', 'BV', 'BZ', 'CC', 'CK', 'CN', 'CV', 'CW', 'CX', 'DM', 'EH', 'FJ', 'FK', 'FM', 'FO', 'GD', 'GF', 'GG', 'GI', 'GL', 'GP', 'GS', 'GU', 'GW', 'GY', 'HK', 'HM', 'IM', 'IO', 'IS', 'JE', 'KI', 'KM', 'KN', 'KY', 'LC', 'LI', 'LS', 'LT', 'LU', 'LV', 'MC', 'MD', 'ME', 'MF', 'MH', 'MK', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MW', 'NC', 'NF', 'NR', 'NU', 'PF', 'PG', 'PM', 'PN', 'PW', 'RE', 'RO', 'SB', 'SC', 'SH', 'SI', 'SJ', 'SM', 'SR', 'ST', 'SX', 'TC', 'TF', 'TG', 'TK', 'TL', 'TO', 'TT', 'TV', 'TW', 'UM', 'VA', 'VC', 'VG', 'VI', 'VU', 'WF', 'WS', 'YT', 'ZA');
|
||||||
|
|
||||||
DELETE FROM countries
|
DELETE FROM countries
|
||||||
WHERE app_code = 'lalu'
|
WHERE app_code = 'lalu'
|
||||||
AND country_code IN ('AG', 'AQ', 'AS', 'BA', 'BQ', 'CK', 'EH', 'FK', 'FO', 'GP', 'GW', 'HK', 'HM', 'IO', 'KN', 'MH', 'MK', 'MP', 'PF', 'PG', 'PM', 'PN', 'SH', 'ST', 'TC', 'TF', 'TT', 'TW', 'UM', 'VC', 'VG', 'VI', 'WF');
|
AND country_code IN ('AD', 'AG', 'AI', 'AQ', 'AS', 'AW', 'AX', 'AZ', 'BA', 'BB', 'BF', 'BI', 'BL', 'BM', 'BN', 'BQ', 'BS', 'BT', 'BV', 'BZ', 'CC', 'CK', 'CN', 'CV', 'CW', 'CX', 'DM', 'EH', 'FJ', 'FK', 'FM', 'FO', 'GD', 'GF', 'GG', 'GI', 'GL', 'GP', 'GS', 'GU', 'GW', 'GY', 'HK', 'HM', 'IM', 'IO', 'IS', 'JE', 'KI', 'KM', 'KN', 'KY', 'LC', 'LI', 'LS', 'LT', 'LU', 'LV', 'MC', 'MD', 'ME', 'MF', 'MH', 'MK', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MW', 'NC', 'NF', 'NR', 'NU', 'PF', 'PG', 'PM', 'PN', 'PW', 'RE', 'RO', 'SB', 'SC', 'SH', 'SI', 'SJ', 'SM', 'SR', 'ST', 'SX', 'TC', 'TF', 'TG', 'TK', 'TL', 'TO', 'TT', 'TV', 'TW', 'UM', 'VA', 'VC', 'VG', 'VI', 'VU', 'WF', 'WS', 'YT', 'ZA');
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_display_user_ids (
|
CREATE TABLE IF NOT EXISTS user_display_user_ids (
|
||||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||||||
@ -1517,6 +1286,22 @@ CREATE TABLE IF NOT EXISTS login_ip_risk_decisions (
|
|||||||
KEY idx_login_ip_risk_decisions_expires (app_code, expires_at_ms)
|
KEY idx_login_ip_risk_decisions_expires (app_code, expires_at_ms)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS login_risk_country_blocks (
|
||||||
|
app_code VARCHAR(32) NOT NULL,
|
||||||
|
block_id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
keyword VARCHAR(128) NOT NULL,
|
||||||
|
country_code VARCHAR(3) NOT NULL DEFAULT '',
|
||||||
|
enabled TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_by_user_id BIGINT NULL,
|
||||||
|
updated_by_user_id BIGINT NULL,
|
||||||
|
created_at_ms BIGINT NOT NULL,
|
||||||
|
updated_at_ms BIGINT NOT NULL,
|
||||||
|
PRIMARY KEY (block_id),
|
||||||
|
UNIQUE KEY uk_login_risk_country_blocks_keyword (app_code, keyword),
|
||||||
|
KEY idx_login_risk_country_blocks_enabled (app_code, enabled, country_code),
|
||||||
|
KEY idx_login_risk_country_blocks_updated (app_code, updated_at_ms)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 本地联调测试账号。
|
-- 本地联调测试账号。
|
||||||
-- 密码登录仍以 users.current_display_user_id 为账号,password_accounts 只保存 user_id 维度密码身份。
|
-- 密码登录仍以 users.current_display_user_id 为账号,password_accounts 只保存 user_id 维度密码身份。
|
||||||
INSERT INTO users (
|
INSERT INTO users (
|
||||||
|
|||||||
@ -59,3 +59,15 @@ type LoginIPRiskDecision struct {
|
|||||||
ExpiresAtMs int64
|
ExpiresAtMs int64
|
||||||
CreatedAtMs int64
|
CreatedAtMs int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoginRiskCountryBlock 是后台维护的国家屏蔽词事实。
|
||||||
|
// CountryCode 为空时仅作为客户端词表下发;服务端 IP 风控只能稳定消费 provider 返回的国家码。
|
||||||
|
type LoginRiskCountryBlock struct {
|
||||||
|
AppCode string
|
||||||
|
BlockID int64
|
||||||
|
Keyword string
|
||||||
|
CountryCode string
|
||||||
|
Enabled bool
|
||||||
|
CreatedAtMs int64
|
||||||
|
UpdatedAtMs int64
|
||||||
|
}
|
||||||
|
|||||||
91
services/user-service/internal/service/auth/risk_policy.go
Normal file
91
services/user-service/internal/service/auth/risk_policy.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hyapp/pkg/appcode"
|
||||||
|
authdomain "hyapp/services/user-service/internal/domain/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListLoginRiskBlockedCountries 返回当前 App 实际生效的登录地区风控词表。
|
||||||
|
// 静态配置和后台动态配置都下发给 App;App 可异步做本地风险提示,最终封禁仍以服务端 worker 为准。
|
||||||
|
func (s *Service) ListLoginRiskBlockedCountries(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
|
||||||
|
if !s.loginRiskPolicy.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
appCode := appcode.FromContext(ctx)
|
||||||
|
blocks := make([]authdomain.LoginRiskCountryBlock, 0, len(s.loginRiskPolicy.UnsupportedCountries))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, country := range s.loginRiskPolicy.UnsupportedCountries {
|
||||||
|
countryCode := normalizeCountryCode(country)
|
||||||
|
if countryCode == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
block := authdomain.LoginRiskCountryBlock{
|
||||||
|
AppCode: appCode,
|
||||||
|
Keyword: countryCode,
|
||||||
|
CountryCode: countryCode,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
blocks = appendUniqueCountryBlock(blocks, seen, block)
|
||||||
|
}
|
||||||
|
if s.authRepository != nil {
|
||||||
|
dynamicBlocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, block := range dynamicBlocks {
|
||||||
|
block.AppCode = appcode.Normalize(block.AppCode)
|
||||||
|
block.Keyword = strings.TrimSpace(block.Keyword)
|
||||||
|
block.CountryCode = normalizeCountryCode(block.CountryCode)
|
||||||
|
block.Enabled = true
|
||||||
|
if block.Keyword == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
blocks = appendUniqueCountryBlock(blocks, seen, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.SliceStable(blocks, func(i, j int) bool {
|
||||||
|
if blocks[i].CountryCode != blocks[j].CountryCode {
|
||||||
|
return blocks[i].CountryCode < blocks[j].CountryCode
|
||||||
|
}
|
||||||
|
return strings.ToLower(blocks[i].Keyword) < strings.ToLower(blocks[j].Keyword)
|
||||||
|
})
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueCountryBlock(blocks []authdomain.LoginRiskCountryBlock, seen map[string]struct{}, block authdomain.LoginRiskCountryBlock) []authdomain.LoginRiskCountryBlock {
|
||||||
|
key := strings.ToLower(block.CountryCode + "\x00" + block.Keyword)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
return append(blocks, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) countryBlockedByStaticPolicy(countryCode string) bool {
|
||||||
|
for _, country := range s.loginRiskPolicy.UnsupportedCountries {
|
||||||
|
if normalizeCountryCode(country) == countryCode {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) countryBlockedByDynamicPolicy(ctx context.Context, countryCode string) (bool, error) {
|
||||||
|
if s.authRepository == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
blocks, err := s.authRepository.ListEnabledLoginRiskCountryBlocks(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, block := range blocks {
|
||||||
|
if normalizeCountryCode(block.CountryCode) == countryCode || normalizeCountryCode(block.Keyword) == countryCode {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
@ -76,7 +76,7 @@ func (s *Service) processLoginIPRiskJob(ctx context.Context, job authdomain.Logi
|
|||||||
}
|
}
|
||||||
|
|
||||||
result, attempts := s.resolveIPCountry(ctx, job, policy)
|
result, attempts := s.resolveIPCountry(ctx, job, policy)
|
||||||
decision := s.decisionForCountry(result.CountryCode)
|
decision := s.decisionForCountry(ctx, result.CountryCode)
|
||||||
ttl := s.ttlForDecision(decision)
|
ttl := s.ttlForDecision(decision)
|
||||||
expiresAtMs := nowMs + ttl.Milliseconds()
|
expiresAtMs := nowMs + ttl.Milliseconds()
|
||||||
ipDecision := IPDecision{
|
ipDecision := IPDecision{
|
||||||
@ -174,15 +174,22 @@ func (s *Service) resolveIPCountry(ctx context.Context, job authdomain.LoginIPRi
|
|||||||
return IPGeoResult{}, attempts
|
return IPGeoResult{}, attempts
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) decisionForCountry(countryCode string) string {
|
func (s *Service) decisionForCountry(ctx context.Context, countryCode string) string {
|
||||||
countryCode = normalizeCountryCode(countryCode)
|
countryCode = normalizeCountryCode(countryCode)
|
||||||
if countryCode == "" {
|
if countryCode == "" {
|
||||||
return authdomain.LoginIPRiskDecisionUnknown
|
return authdomain.LoginIPRiskDecisionUnknown
|
||||||
}
|
}
|
||||||
for _, country := range s.loginRiskPolicy.UnsupportedCountries {
|
if s.countryBlockedByStaticPolicy(countryCode) {
|
||||||
if normalizeCountryCode(country) == countryCode {
|
return authdomain.LoginIPRiskDecisionBlocked
|
||||||
return authdomain.LoginIPRiskDecisionBlocked
|
}
|
||||||
}
|
dynamicBlocked, err := s.countryBlockedByDynamicPolicy(ctx, countryCode)
|
||||||
|
if err != nil {
|
||||||
|
// 动态配置读取失败不能扩大误伤面;保留静态配置阻断,其余按 fail-open 放行并记录。
|
||||||
|
logx.Warn(ctx, "login_risk_country_blocks_read_failed", slog.String("component", "user_auth_risk"), slog.String("country", countryCode), slog.String("error", err.Error()))
|
||||||
|
return authdomain.LoginIPRiskDecisionAllowed
|
||||||
|
}
|
||||||
|
if dynamicBlocked {
|
||||||
|
return authdomain.LoginIPRiskDecisionBlocked
|
||||||
}
|
}
|
||||||
return authdomain.LoginIPRiskDecisionAllowed
|
return authdomain.LoginIPRiskDecisionAllowed
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,8 @@ type AuthRepository interface {
|
|||||||
CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
|
CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error
|
||||||
// CreateLoginIPRiskDecision 持久化 IP 风控决策审计。
|
// CreateLoginIPRiskDecision 持久化 IP 风控决策审计。
|
||||||
CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error
|
CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error
|
||||||
|
// ListEnabledLoginRiskCountryBlocks 返回当前 App 的动态屏蔽国家词表。
|
||||||
|
ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error)
|
||||||
// MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。
|
// MarkLoginAuditBlocked 把登录成功后被异步风控撤销的审计行更新成阻断态。
|
||||||
MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error
|
MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -326,6 +326,61 @@ func TestLoginIPRiskWorkerRevokesBlockedCountrySession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoginIPRiskWorkerUsesDynamicCountryBlocks(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
seedCountry(t, repository, "MY")
|
||||||
|
now := time.UnixMilli(1000)
|
||||||
|
if _, err := repository.RawDB().ExecContext(ctx, `
|
||||||
|
INSERT INTO login_risk_country_blocks (
|
||||||
|
app_code, keyword, country_code, enabled, created_at_ms, updated_at_ms
|
||||||
|
)
|
||||||
|
VALUES ('lalu', 'Malaysia', 'MY', 1, 1000, 1000)
|
||||||
|
`); err != nil {
|
||||||
|
t.Fatalf("seed dynamic country block failed: %v", err)
|
||||||
|
}
|
||||||
|
authSvc := newAuthService(repository, &now, []int64{900021}, []string{"100021"},
|
||||||
|
authservice.WithLoginRiskPolicy(authservice.LoginRiskPolicy{
|
||||||
|
Enabled: true,
|
||||||
|
UnsupportedCountries: nil,
|
||||||
|
BlockedTTL: time.Hour,
|
||||||
|
AllowedTTL: time.Hour,
|
||||||
|
UnknownTTL: time.Minute,
|
||||||
|
ProviderTimeout: 50 * time.Millisecond,
|
||||||
|
ProviderNames: []string{"edge_country"},
|
||||||
|
DenylistTTL: time.Hour,
|
||||||
|
}),
|
||||||
|
authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders([]string{"edge_country"})),
|
||||||
|
)
|
||||||
|
|
||||||
|
token, _, err := authSvc.LoginThirdParty(ctx, "wechat", "openid-risk-dynamic", thirdPartyRegistration("ios"), authservice.Meta{
|
||||||
|
AppCode: "lalu",
|
||||||
|
RequestID: "req-risk-dynamic",
|
||||||
|
ClientIP: "203.0.113.88",
|
||||||
|
CountryByIP: "MY",
|
||||||
|
Platform: "ios",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoginThirdParty failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now = time.UnixMilli(2000)
|
||||||
|
processed, err := authSvc.ProcessLoginIPRiskBatch(ctx, authservice.LoginIPRiskWorkerOptions{WorkerID: "risk-test", LockTTL: time.Second, BatchSize: 10})
|
||||||
|
if err != nil || processed != 1 {
|
||||||
|
t.Fatalf("risk worker mismatch: processed=%d err=%v", processed, err)
|
||||||
|
}
|
||||||
|
if _, err := authSvc.RefreshToken(ctx, token.RefreshToken, "dev-ios", authservice.Meta{AppCode: "lalu", RequestID: "req-refresh-dynamic"}); !xerr.IsCode(err, xerr.SessionRevoked) {
|
||||||
|
t.Fatalf("expected revoked refresh session, got %v", err)
|
||||||
|
}
|
||||||
|
blocks, err := authSvc.ListLoginRiskBlockedCountries(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListLoginRiskBlockedCountries failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(blocks) != 1 || blocks[0].CountryCode != "MY" || blocks[0].Keyword != "Malaysia" {
|
||||||
|
t.Fatalf("dynamic block list mismatch: %+v", blocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIssueAccessTokenForSessionCarriesCompletedOnboardingStatusWithoutRotatingRefresh(t *testing.T) {
|
func TestIssueAccessTokenForSessionCarriesCompletedOnboardingStatusWithoutRotatingRefresh(t *testing.T) {
|
||||||
// CompleteOnboarding 后只需要替换 access token;refresh token 原文不可从数据库反查,也不应被静默轮换。
|
// CompleteOnboarding 后只需要替换 access token;refresh token 原文不可从数据库反查,也不应被静默轮换。
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
@ -103,10 +104,45 @@ func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision aut
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT app_code, block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
|
||||||
|
FROM login_risk_country_blocks
|
||||||
|
WHERE app_code = ? AND enabled = 1
|
||||||
|
ORDER BY country_code ASC, keyword ASC, block_id ASC
|
||||||
|
`, appcode.FromContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
blocks := make([]authdomain.LoginRiskCountryBlock, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var block authdomain.LoginRiskCountryBlock
|
||||||
|
if err := rows.Scan(&block.AppCode, &block.BlockID, &block.Keyword, &block.CountryCode, &block.Enabled, &block.CreatedAtMs, &block.UpdatedAtMs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
block.CountryCode = normalizeCountryCode(block.CountryCode)
|
||||||
|
blocks = append(blocks, block)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return blocks, nil
|
||||||
|
}
|
||||||
|
|
||||||
type loginIPRiskJobScanner interface {
|
type loginIPRiskJobScanner interface {
|
||||||
Scan(dest ...any) error
|
Scan(dest ...any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeCountryCode(country string) string {
|
||||||
|
country = strings.ToUpper(strings.TrimSpace(country))
|
||||||
|
if len(country) < 2 || len(country) > 3 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return country
|
||||||
|
}
|
||||||
|
|
||||||
func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) {
|
func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) {
|
||||||
var job authdomain.LoginIPRiskJob
|
var job authdomain.LoginIPRiskJob
|
||||||
err := scanner.Scan(
|
err := scanner.Scan(
|
||||||
|
|||||||
@ -311,6 +311,11 @@ func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision aut
|
|||||||
return r.Repository.AuthRepository().CreateLoginIPRiskDecision(ctx, decision)
|
return r.Repository.AuthRepository().CreateLoginIPRiskDecision(ctx, decision)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListEnabledLoginRiskCountryBlocks 让测试 wrapper 继续满足认证风控接口。
|
||||||
|
func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
|
||||||
|
return r.Repository.AuthRepository().ListEnabledLoginRiskCountryBlocks(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// MarkLoginAuditBlocked 让测试 wrapper 继续满足认证接口。
|
// MarkLoginAuditBlocked 让测试 wrapper 继续满足认证接口。
|
||||||
func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error {
|
func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error {
|
||||||
return r.Repository.AuthRepository().MarkLoginAuditBlocked(ctx, requestID, userID, countryCode, blockReason)
|
return r.Repository.AuthRepository().MarkLoginAuditBlocked(ctx, requestID, userID, countryCode, blockReason)
|
||||||
|
|||||||
@ -566,6 +566,29 @@ func (s *Server) ListRegistrationCountries(ctx context.Context, req *userv1.List
|
|||||||
return &userv1.ListRegistrationCountriesResponse{Countries: result}, nil
|
return &userv1.ListRegistrationCountriesResponse{Countries: result}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListLoginRiskBlockedCountries 返回 App 登录后异步风控可消费的地区屏蔽词表。
|
||||||
|
func (s *Server) ListLoginRiskBlockedCountries(ctx context.Context, req *userv1.ListLoginRiskBlockedCountriesRequest) (*userv1.ListLoginRiskBlockedCountriesResponse, error) {
|
||||||
|
ctx = contextWithApp(ctx, req.GetMeta())
|
||||||
|
blocks, err := s.authSvc.ListLoginRiskBlockedCountries(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
result := make([]*userv1.LoginRiskBlockedCountry, 0, len(blocks))
|
||||||
|
var updatedAtMs int64
|
||||||
|
for _, block := range blocks {
|
||||||
|
if block.UpdatedAtMs > updatedAtMs {
|
||||||
|
updatedAtMs = block.UpdatedAtMs
|
||||||
|
}
|
||||||
|
result = append(result, &userv1.LoginRiskBlockedCountry{
|
||||||
|
CountryCode: block.CountryCode,
|
||||||
|
Keyword: block.Keyword,
|
||||||
|
UpdatedAtMs: block.UpdatedAtMs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &userv1.ListLoginRiskBlockedCountriesResponse{Countries: result, UpdatedAtMs: updatedAtMs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListRegions 返回区域列表。
|
// ListRegions 返回区域列表。
|
||||||
func (s *Server) ListRegions(ctx context.Context, req *userv1.ListRegionsRequest) (*userv1.ListRegionsResponse, error) {
|
func (s *Server) ListRegions(ctx context.Context, req *userv1.ListRegionsRequest) (*userv1.ListRegionsResponse, error) {
|
||||||
ctx = contextWithApp(ctx, req.GetMeta())
|
ctx = contextWithApp(ctx, req.GetMeta())
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
FROM golang:1.26.3-alpine AS builder
|
FROM golang:1.26.3-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@ -9,12 +10,16 @@ ENV GOPROXY=${GOPROXY} GOSUMDB=${GOSUMDB}
|
|||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY api/go.mod api/go.sum ./api/
|
COPY api/go.mod api/go.sum ./api/
|
||||||
RUN GOWORK=off go mod download
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
GOWORK=off go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./services/wallet-service/cmd/server
|
RUN --mount=type=cache,id=hyapp-go-mod,target=/go/pkg/mod \
|
||||||
RUN GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/grpc-health-probe ./cmd/grpc-health-probe
|
--mount=type=cache,id=hyapp-go-build,target=/root/.cache/go-build \
|
||||||
|
mkdir -p /out && \
|
||||||
|
GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/ ./services/wallet-service/cmd/server ./cmd/grpc-health-probe
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user